diff --git a/api/entity.go b/api/entity.go new file mode 100644 index 00000000..f3aaece5 --- /dev/null +++ b/api/entity.go @@ -0,0 +1,125 @@ +package api + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/microsoft/durabletask-go/internal/helpers" + "github.com/microsoft/durabletask-go/internal/protos" + "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +// EntityID uniquely identifies an entity by its name and key. +type EntityID struct { + Name string + Key string +} + +// NewEntityID creates a new EntityID with the specified name and key. +func NewEntityID(name string, key string) EntityID { + if err := helpers.ValidateEntityName(name); err != nil { + panic(err) + } + return EntityID{Name: strings.ToLower(name), Key: key} +} + +// String returns the entity instance ID in the format "@@". +func (e EntityID) String() string { + return fmt.Sprintf("@%s@%s", strings.ToLower(e.Name), e.Key) +} + +// EntityIDFromString parses an entity instance ID string in the format "@@". +func EntityIDFromString(s string) (EntityID, error) { + name, key, err := helpers.ParseEntityInstanceID(s) + if err != nil { + return EntityID{}, err + } + return EntityID{Name: name, Key: key}, nil +} + +// EntityMetadata contains metadata about an entity instance. +type EntityMetadata struct { + InstanceID EntityID + LastModifiedTime time.Time + BacklogQueueSize int32 + LockedBy string + SerializedState string +} + +// SignalEntityOptions is a functional option type for signaling an entity. +type SignalEntityOptions func(*protos.SignalEntityRequest) error + +// WithSignalInput configures the input for an entity signal. +func WithSignalInput(input any) SignalEntityOptions { + return func(req *protos.SignalEntityRequest) error { + bytes, err := json.Marshal(input) + if err != nil { + return err + } + req.Input = wrapperspb.String(string(bytes)) + return nil + } +} + +// WithRawSignalInput configures a raw string input for an entity signal. +func WithRawSignalInput(input string) SignalEntityOptions { + return func(req *protos.SignalEntityRequest) error { + req.Input = wrapperspb.String(input) + return nil + } +} + +// WithSignalScheduledTime configures a scheduled time for the entity signal. +func WithSignalScheduledTime(t time.Time) SignalEntityOptions { + return func(req *protos.SignalEntityRequest) error { + req.ScheduledTime = timestamppb.New(t) + return nil + } +} + +// EntityQuery defines filter criteria for querying entities. +type EntityQuery struct { + // InstanceIDStartsWith filters entities whose instance ID starts with this prefix. + InstanceIDStartsWith string + // LastModifiedFrom filters entities modified on or after this time. + LastModifiedFrom time.Time + // LastModifiedTo filters entities modified before this time. + LastModifiedTo time.Time + // IncludeState whether to include entity state in the results. + IncludeState bool + // IncludeTransient whether to include transient (stateless) entities. + IncludeTransient bool + // PageSize limits the number of entities returned per page. + PageSize int32 + // ContinuationToken for fetching the next page of results. + ContinuationToken string +} + +// EntityQueryResults contains the results of an entity query. +type EntityQueryResults struct { + Entities []*EntityMetadata + ContinuationToken string +} + +// CleanEntityStorageRequest contains options for cleaning entity storage. +type CleanEntityStorageRequest struct { + // ContinuationToken for resuming a previous cleanup operation. + ContinuationToken string + // RemoveEmptyEntities removes entities with no state and no locks. + RemoveEmptyEntities bool + // ReleaseOrphanedLocks releases locks held by non-running orchestrations. + ReleaseOrphanedLocks bool +} + +// CleanEntityStorageResult contains the results of a cleanup operation. +type CleanEntityStorageResult struct { + // EmptyEntitiesRemoved is the number of empty entities removed. + EmptyEntitiesRemoved int32 + // OrphanedLocksReleased is the number of orphaned locks released. + OrphanedLocksReleased int32 + // ContinuationToken for resuming cleanup. Empty if complete. + ContinuationToken string +} diff --git a/api/entity_test.go b/api/entity_test.go new file mode 100644 index 00000000..d2b14b18 --- /dev/null +++ b/api/entity_test.go @@ -0,0 +1,45 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_API_EntityID_String(t *testing.T) { + id := NewEntityID("Counter", "myCounter") + assert.Equal(t, "@counter@myCounter", id.String()) +} + +func Test_API_EntityIDFromString(t *testing.T) { + tests := []struct { + name string + input string + want EntityID + wantErr bool + }{ + {name: "valid", input: "@counter@key1", want: EntityID{Name: "counter", Key: "key1"}}, + {name: "empty key", input: "@entity@", want: EntityID{Name: "entity", Key: ""}}, + {name: "invalid empty name", input: "@@key1", wantErr: true}, + {name: "invalid no prefix", input: "no-at-sign", wantErr: true}, + {name: "invalid no second @", input: "@onlyone", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := EntityIDFromString(tt.input) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_API_NewEntityID_InvalidNamePanics(t *testing.T) { + assert.Panics(t, func() { NewEntityID("", "key") }) + assert.Panics(t, func() { NewEntityID("bad@name", "key") }) +} diff --git a/api/orchestration.go b/api/orchestration.go index 12bc9ef3..dfe0ac44 100644 --- a/api/orchestration.go +++ b/api/orchestration.go @@ -80,6 +80,9 @@ type PurgeOptions func(*protos.PurgeInstancesRequest) error // a random UUID value will be used for the orchestration instance ID. func WithInstanceID(id InstanceID) NewOrchestrationOptions { return func(req *protos.CreateInstanceRequest) error { + if err := helpers.ValidateOrchestrationInstanceID(string(id)); err != nil { + return err + } req.InstanceId = string(id) return nil } diff --git a/api/orchestration_test.go b/api/orchestration_test.go new file mode 100644 index 00000000..ef1c738a --- /dev/null +++ b/api/orchestration_test.go @@ -0,0 +1,23 @@ +package api + +import ( + "testing" + + "github.com/microsoft/durabletask-go/internal/protos" + "github.com/stretchr/testify/require" +) + +func Test_API_WithInstanceID_RejectsEntityFormat(t *testing.T) { + req := &protos.CreateInstanceRequest{} + + err := WithInstanceID(InstanceID("@counter@key"))(req) + require.Error(t, err) +} + +func Test_API_WithInstanceID_AllowsNormalValue(t *testing.T) { + req := &protos.CreateInstanceRequest{} + + err := WithInstanceID(InstanceID("my-instance"))(req) + require.NoError(t, err) + require.Equal(t, "my-instance", req.InstanceId) +} diff --git a/backend/backend.go b/backend/backend.go index f5c77758..778a63c4 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -110,6 +110,25 @@ type Backend interface { PurgeOrchestrationState(context.Context, api.InstanceID) error } +// EntityBackend is an optional interface that backends can implement to support +// entity-specific storage operations like querying and cleanup. +// If a backend does not implement this interface, entity queries and cleanup +// operations will not be available through the in-process client. +type EntityBackend interface { + Backend + + // GetEntityMetadata retrieves metadata for a specific entity instance. + // Returns nil if the entity doesn't exist. + GetEntityMetadata(context.Context, api.EntityID, bool) (*api.EntityMetadata, error) + + // QueryEntities queries entity instances matching the specified filter criteria. + QueryEntities(context.Context, api.EntityQuery) (*api.EntityQueryResults, error) + + // CleanEntityStorage performs garbage collection on entity storage, removing + // empty entities and releasing orphaned locks. + CleanEntityStorage(context.Context, api.CleanEntityStorageRequest) (*api.CleanEntityStorageResult, error) +} + // MarshalHistoryEvent serializes the [HistoryEvent] into a protobuf byte array. func MarshalHistoryEvent(e *HistoryEvent) ([]byte, error) { if bytes, err := proto.Marshal(e); err != nil { diff --git a/backend/client.go b/backend/client.go index aff39184..983093cb 100644 --- a/backend/client.go +++ b/backend/client.go @@ -2,6 +2,8 @@ package backend import ( "context" + "encoding/json" + "errors" "fmt" "time" @@ -9,6 +11,7 @@ import ( "github.com/google/uuid" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/types/known/wrapperspb" "github.com/microsoft/durabletask-go/api" "github.com/microsoft/durabletask-go/internal/helpers" @@ -27,6 +30,17 @@ type TaskHubClient interface { PurgeOrchestrationState(ctx context.Context, id api.InstanceID, opts ...api.PurgeOptions) error } +// EntityTaskHubClient is an optional extension of [TaskHubClient] that adds entity-specific +// operations. Clients returned by [NewTaskHubClient] always implement this interface. +// The gRPC client ([TaskHubGrpcClient]) also implements this interface. +type EntityTaskHubClient interface { + TaskHubClient + SignalEntity(ctx context.Context, entityID api.EntityID, operationName string, opts ...api.SignalEntityOptions) error + FetchEntityMetadata(ctx context.Context, entityID api.EntityID, includeState bool) (*api.EntityMetadata, error) + QueryEntities(ctx context.Context, query api.EntityQuery) (*api.EntityQueryResults, error) + CleanEntityStorage(ctx context.Context, req api.CleanEntityStorageRequest) (*api.CleanEntityStorageResult, error) +} + type backendClient struct { be Backend } @@ -52,6 +66,9 @@ func (c *backendClient) ScheduleNewOrchestration(ctx context.Context, orchestrat } req.InstanceId = u.String() } + if err := helpers.ValidateOrchestrationInstanceID(req.InstanceId); err != nil { + return api.EmptyInstanceID, err + } var span trace.Span ctx, span = helpers.StartNewCreateOrchestrationSpan(ctx, req.Name, req.Version.GetValue(), req.InstanceId) @@ -205,3 +222,116 @@ func (c *backendClient) PurgeOrchestrationState(ctx context.Context, id api.Inst } return nil } + +// SignalEntity sends a fire-and-forget signal to an entity, triggering the specified operation. +// +// If the entity doesn't exist, it will be created automatically when the signal is processed. +func (c *backendClient) SignalEntity(ctx context.Context, entityID api.EntityID, operationName string, opts ...api.SignalEntityOptions) error { + if err := helpers.ValidateEntityName(entityID.Name); err != nil { + return err + } + + req := &protos.SignalEntityRequest{ + InstanceId: entityID.String(), + Name: operationName, + } + for _, configure := range opts { + if err := configure(req); err != nil { + return fmt.Errorf("failed to configure signal entity request: %w", err) + } + } + + // Ensure the entity orchestration instance exists. Create with IGNORE policy + // so it's a no-op if the instance already exists. + startEvent := helpers.NewExecutionStartedEvent(entityID.Name, req.InstanceId, nil, nil, nil, nil) + createErr := c.be.CreateOrchestrationInstance(ctx, startEvent, WithOrchestrationIdReusePolicy(&protos.OrchestrationIdReusePolicy{ + Action: protos.CreateOrchestrationAction_IGNORE, + OperationStatus: []protos.OrchestrationStatus{protos.OrchestrationStatus_ORCHESTRATION_STATUS_RUNNING}, + })) + if createErr != nil && !errors.Is(createErr, api.ErrDuplicateInstance) && !errors.Is(createErr, api.ErrIgnoreInstance) { + return fmt.Errorf("failed to create entity instance: %w", createErr) + } + + // Build the .NET-compatible EntityRequestMessage payload with isSignal=true. + requestID := req.RequestId + if requestID == "" { + requestID = uuid.New().String() + } + reqMsg := helpers.EntityRequestMessage{ + ID: requestID, + IsSignal: true, + Operation: req.Name, + } + if req.Input != nil { + reqMsg.Input = req.Input.GetValue() + } + payload, err := json.Marshal(reqMsg) + if err != nil { + return fmt.Errorf("failed to marshal signal request: %w", err) + } + + e := helpers.NewEventRaisedEvent(helpers.EntityRequestEventName, wrapperspb.String(string(payload))) + if req.ScheduledTime != nil { + e.Timestamp = req.ScheduledTime + } + if err := c.be.AddNewOrchestrationEvent(ctx, api.InstanceID(req.InstanceId), e); err != nil { + return fmt.Errorf("failed to signal entity: %w", err) + } + return nil +} + +// FetchEntityMetadata retrieves metadata about an entity instance. +// +// Returns nil if the entity doesn't exist. +// If the backend implements [EntityBackend], its native entity storage is used. +// Otherwise, falls back to orchestration metadata. +func (c *backendClient) FetchEntityMetadata(ctx context.Context, entityID api.EntityID, includeState bool) (*api.EntityMetadata, error) { + if err := helpers.ValidateEntityName(entityID.Name); err != nil { + return nil, err + } + if eb, ok := c.be.(EntityBackend); ok { + return eb.GetEntityMetadata(ctx, entityID, includeState) + } + + // Fallback: entities are backed by orchestrations + iid := api.InstanceID(entityID.String()) + metadata, err := c.be.GetOrchestrationMetadata(ctx, iid) + if err != nil { + if errors.Is(err, api.ErrInstanceNotFound) { + return nil, nil + } + return nil, fmt.Errorf("failed to get entity metadata: %w", err) + } + if metadata == nil { + return nil, nil + } + + result := &api.EntityMetadata{ + InstanceID: entityID, + LastModifiedTime: metadata.LastUpdatedAt, + } + if includeState { + result.SerializedState = metadata.SerializedCustomStatus + } + return result, nil +} + +// QueryEntities queries entities matching the specified filter criteria. +// +// Requires the backend to implement [EntityBackend]. +func (c *backendClient) QueryEntities(ctx context.Context, query api.EntityQuery) (*api.EntityQueryResults, error) { + if eb, ok := c.be.(EntityBackend); ok { + return eb.QueryEntities(ctx, query) + } + return nil, fmt.Errorf("QueryEntities requires the backend to implement EntityBackend") +} + +// CleanEntityStorage performs garbage collection on entity storage. +// +// Requires the backend to implement [EntityBackend]. +func (c *backendClient) CleanEntityStorage(ctx context.Context, req api.CleanEntityStorageRequest) (*api.CleanEntityStorageResult, error) { + if eb, ok := c.be.(EntityBackend); ok { + return eb.CleanEntityStorage(ctx, req) + } + return nil, fmt.Errorf("CleanEntityStorage requires the backend to implement EntityBackend") +} diff --git a/backend/executor.go b/backend/executor.go index 570988cd..ea2a66ff 100644 --- a/backend/executor.go +++ b/backend/executor.go @@ -2,6 +2,7 @@ package backend import ( context "context" + "encoding/json" "errors" "fmt" "strconv" @@ -10,6 +11,7 @@ import ( "time" "github.com/cenkalti/backoff/v4" + "github.com/google/uuid" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" @@ -39,6 +41,50 @@ type activityExecutionResult struct { pending chan string } +type entityExecutionResult struct { + response *protos.EntityBatchResult + complete chan struct{} + pending chan string +} + +type entityExecutionQueue struct { + mu sync.Mutex + keys []string +} + +func (q *entityExecutionQueue) Enqueue(key string) { + q.mu.Lock() + defer q.mu.Unlock() + q.keys = append(q.keys, key) +} + +func (q *entityExecutionQueue) Dequeue() (string, bool) { + q.mu.Lock() + defer q.mu.Unlock() + + if len(q.keys) == 0 { + return "", false + } + + key := q.keys[0] + q.keys = q.keys[1:] + return key, true +} + +func (q *entityExecutionQueue) Remove(key string) bool { + q.mu.Lock() + defer q.mu.Unlock() + + for i, candidate := range q.keys { + if candidate == key { + q.keys = append(q.keys[:i], q.keys[i+1:]...) + return true + } + } + + return false +} + type Executor interface { ExecuteOrchestrator(ctx context.Context, iid api.InstanceID, oldEvents []*protos.HistoryEvent, newEvents []*protos.HistoryEvent) (*ExecutionResults, error) ExecuteActivity(context.Context, api.InstanceID, *protos.HistoryEvent) (*protos.HistoryEvent, error) @@ -50,6 +96,8 @@ type grpcExecutor struct { workItemQueue chan *protos.WorkItem pendingOrchestrators *sync.Map // map[api.InstanceID]*ExecutionResults pendingActivities *sync.Map // map[string]*activityExecutionResult + pendingEntities *sync.Map // map[string]*entityExecutionResult + entityQueue *entityExecutionQueue backend Backend logger Logger onWorkItemConnection func(context.Context) error @@ -87,6 +135,8 @@ func NewGrpcExecutor(be Backend, logger Logger, opts ...grpcExecutorOptions) (ex logger: logger, pendingOrchestrators: &sync.Map{}, pendingActivities: &sync.Map{}, + pendingEntities: &sync.Map{}, + entityQueue: &entityExecutionQueue{}, } for _, opt := range opts { @@ -208,10 +258,60 @@ func (g *grpcExecutor) Shutdown(ctx context.Context) error { } return true }) + g.pendingEntities.Range(func(_, value any) bool { + p, ok := value.(*entityExecutionResult) + if ok { + close(p.complete) + } + return true + }) return nil } +// ExecuteEntity implements Executor +func (executor *grpcExecutor) ExecuteEntity(ctx context.Context, req *protos.EntityBatchRequest) (*protos.EntityBatchResult, error) { + key := req.InstanceId + result := &entityExecutionResult{complete: make(chan struct{})} + if _, loaded := executor.pendingEntities.LoadOrStore(key, result); loaded { + return nil, fmt.Errorf("entity batch for instance '%s' is already pending", key) + } + + workItem := &protos.WorkItem{ + Request: &protos.WorkItem_EntityRequest{ + EntityRequest: req, + }, + } + + select { + case <-ctx.Done(): + executor.pendingEntities.Delete(key) + executor.logger.Warnf("%s: context canceled before dispatching entity work item", key) + return nil, ctx.Err() + case executor.workItemQueue <- workItem: + } + + // Track FIFO completion order for workers that don't send metadata. + // Metadata-based completions remove their corresponding key from this queue. + executor.entityQueue.Enqueue(key) + + select { + case <-ctx.Done(): + executor.pendingEntities.Delete(key) + executor.entityQueue.Remove(key) + executor.logger.Warnf("%s: context canceled before receiving entity result", key) + return nil, ctx.Err() + case <-result.complete: + executor.logger.Debugf("%s: entity got result", key) + if result.response == nil { + executor.entityQueue.Remove(key) + return nil, ErrOperationAborted + } + } + + return result.response, nil +} + // Hello implements protos.TaskHubSidecarServiceServer func (grpcExecutor) Hello(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { return empty, nil @@ -240,6 +340,8 @@ func (g *grpcExecutor) GetWorkItems(req *protos.GetWorkItemsRequest, stream prot pendingActivityCh := make(chan string, 1) pendingOrchestrators := make(map[string]struct{}) pendingOrchestratorCh := make(chan string, 1) + pendingEntities := make(map[string]struct{}) + pendingEntityCh := make(chan string, 1) defer func() { // If there's any pending activity left, remove them for key := range pendingActivities { @@ -258,6 +360,14 @@ func (g *grpcExecutor) GetWorkItems(req *protos.GetWorkItemsRequest, stream prot close(pending.complete) } } + for key := range pendingEntities { + g.logger.Debugf("cleaning up pending entity: %s", key) + p, ok := g.pendingEntities.LoadAndDelete(key) + if ok { + pending := p.(*entityExecutionResult) + close(pending.complete) + } + } }() // The worker client invokes this method, which streams back work-items as they arrive. @@ -285,6 +395,13 @@ func (g *grpcExecutor) GetWorkItems(req *protos.GetWorkItemsRequest, stream prot if ok { p.(*activityExecutionResult).pending = pendingActivityCh } + case *protos.WorkItem_EntityRequest: + key := x.EntityRequest.GetInstanceId() + pendingEntities[key] = struct{}{} + p, ok := g.pendingEntities.Load(key) + if ok { + p.(*entityExecutionResult).pending = pendingEntityCh + } } if err := stream.Send(wi); err != nil { @@ -295,6 +412,8 @@ func (g *grpcExecutor) GetWorkItems(req *protos.GetWorkItemsRequest, stream prot delete(pendingActivities, key) case key := <-pendingOrchestratorCh: delete(pendingOrchestrators, key) + case key := <-pendingEntityCh: + delete(pendingEntities, key) case <-g.streamShutdownChan: return errShuttingDown } @@ -357,6 +476,40 @@ func getActivityExecutionKey(iid string, taskID int32) string { return iid + "/" + strconv.FormatInt(int64(taskID), 10) } +// CompleteEntityTask implements protos.TaskHubSidecarServiceServer +func (g *grpcExecutor) CompleteEntityTask(ctx context.Context, res *protos.EntityBatchResult) (*protos.CompleteTaskResponse, error) { + // EntityBatchResult doesn't include an instance ID field (unlike OrchestratorResponse/ActivityResponse). + // The worker passes the instance ID via gRPC metadata for correlation. + var key string + if md, ok := metadata.FromIncomingContext(ctx); ok { + if values := md.Get("entity-instance-id"); len(values) > 0 { + key = values[0] + } + } + if key == "" { + // Fallback to FIFO queue for non-Go workers that don't send metadata. + var ok bool + if key, ok = g.entityQueue.Dequeue(); !ok { + return emptyCompleteTaskResponse, fmt.Errorf("no pending entity found for completion: missing entity-instance-id metadata") + } + } else { + g.entityQueue.Remove(key) + } + + p, ok := g.pendingEntities.LoadAndDelete(key) + if !ok { + return emptyCompleteTaskResponse, fmt.Errorf("pending entity '%s' was already completed", key) + } + + pending := p.(*entityExecutionResult) + pending.response = res + if pending.pending != nil { + pending.pending <- key + } + close(pending.complete) + return emptyCompleteTaskResponse, nil +} + // CreateTaskHub implements protos.TaskHubSidecarServiceServer func (grpcExecutor) CreateTaskHub(context.Context, *protos.CreateTaskHubRequest) (*protos.CreateTaskHubResponse, error) { return nil, status.Error(codes.Unimplemented, "CreateTaskHub is not implemented") @@ -412,9 +565,62 @@ func (g *grpcExecutor) RaiseEvent(ctx context.Context, req *protos.RaiseEventReq return &protos.RaiseEventResponse{}, nil } +// SignalEntity implements protos.TaskHubSidecarServiceServer +func (g *grpcExecutor) SignalEntity(ctx context.Context, req *protos.SignalEntityRequest) (*protos.SignalEntityResponse, error) { + // Parse entity name from the instance ID + entityID, err := api.EntityIDFromString(req.InstanceId) + if err != nil { + return nil, fmt.Errorf("failed to parse entity instance ID: %w", err) + } + + // Build wire-compatible EntityRequestMessage + requestID := req.RequestId + if requestID == "" { + requestID = uuid.New().String() + } + requestMsg := helpers.EntityRequestMessage{ + ID: requestID, + IsSignal: true, + Operation: req.Name, + } + if req.Input != nil { + requestMsg.Input = req.Input.GetValue() + } + payload, err := json.Marshal(requestMsg) + if err != nil { + return nil, fmt.Errorf("failed to marshal entity request message: %w", err) + } + + // Normalize the instance ID to lowercase for consistent routing + normalizedID := entityID.String() + + // Ensure the entity orchestration instance exists. Create with IGNORE policy + // so it's a no-op if the instance already exists. + startEvent := helpers.NewExecutionStartedEvent(entityID.Name, normalizedID, nil, nil, nil, nil) + createErr := g.backend.CreateOrchestrationInstance(ctx, startEvent, WithOrchestrationIdReusePolicy(&protos.OrchestrationIdReusePolicy{ + Action: protos.CreateOrchestrationAction_IGNORE, + OperationStatus: []protos.OrchestrationStatus{protos.OrchestrationStatus_ORCHESTRATION_STATUS_RUNNING}, + })) + if createErr != nil && !errors.Is(createErr, api.ErrDuplicateInstance) && !errors.Is(createErr, api.ErrIgnoreInstance) { + return nil, fmt.Errorf("failed to create entity instance: %w", createErr) + } + + e := helpers.NewEventRaisedEvent(helpers.EntityRequestEventName, wrapperspb.String(string(payload))) + if req.ScheduledTime != nil { + e.Timestamp = req.ScheduledTime + } + if err := g.backend.AddNewOrchestrationEvent(ctx, api.InstanceID(normalizedID), e); err != nil { + return nil, fmt.Errorf("failed to signal entity: %w", err) + } + return &protos.SignalEntityResponse{}, nil +} + // StartInstance implements protos.TaskHubSidecarServiceServer func (g *grpcExecutor) StartInstance(ctx context.Context, req *protos.CreateInstanceRequest) (*protos.CreateInstanceResponse, error) { instanceID := req.InstanceId + if err := helpers.ValidateOrchestrationInstanceID(instanceID); err != nil { + return nil, err + } ctx, span := helpers.StartNewCreateOrchestrationSpan(ctx, req.Name, req.Version.GetValue(), instanceID) defer span.End() diff --git a/backend/executor_test.go b/backend/executor_test.go new file mode 100644 index 00000000..f33e600d --- /dev/null +++ b/backend/executor_test.go @@ -0,0 +1,135 @@ +package backend + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/microsoft/durabletask-go/api" + "github.com/microsoft/durabletask-go/internal/helpers" + "github.com/microsoft/durabletask-go/internal/protos" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type capturingBackend struct { + created []*HistoryEvent + added []*HistoryEvent + addedTo []api.InstanceID +} + +func (*capturingBackend) CreateTaskHub(context.Context) error { return nil } +func (*capturingBackend) DeleteTaskHub(context.Context) error { return nil } +func (*capturingBackend) Start(context.Context) error { return nil } +func (*capturingBackend) Stop(context.Context) error { return nil } +func (b *capturingBackend) CreateOrchestrationInstance(_ context.Context, e *HistoryEvent, _ ...OrchestrationIdReusePolicyOptions) error { + b.created = append(b.created, e) + return nil +} +func (b *capturingBackend) AddNewOrchestrationEvent(_ context.Context, iid api.InstanceID, e *HistoryEvent) error { + b.addedTo = append(b.addedTo, iid) + b.added = append(b.added, e) + return nil +} +func (*capturingBackend) GetOrchestrationWorkItem(context.Context) (*OrchestrationWorkItem, error) { + return nil, nil +} +func (*capturingBackend) GetOrchestrationRuntimeState(context.Context, *OrchestrationWorkItem) (*OrchestrationRuntimeState, error) { + return nil, nil +} +func (*capturingBackend) GetOrchestrationMetadata(context.Context, api.InstanceID) (*api.OrchestrationMetadata, error) { + return nil, nil +} +func (*capturingBackend) CompleteOrchestrationWorkItem(context.Context, *OrchestrationWorkItem) error { + return nil +} +func (*capturingBackend) AbandonOrchestrationWorkItem(context.Context, *OrchestrationWorkItem) error { + return nil +} +func (*capturingBackend) GetActivityWorkItem(context.Context) (*ActivityWorkItem, error) { + return nil, nil +} +func (*capturingBackend) CompleteActivityWorkItem(context.Context, *ActivityWorkItem) error { + return nil +} +func (*capturingBackend) AbandonActivityWorkItem(context.Context, *ActivityWorkItem) error { + return nil +} +func (*capturingBackend) PurgeOrchestrationState(context.Context, api.InstanceID) error { return nil } + +func Test_GrpcExecutor_ExecuteEntity_RejectsConcurrentInstance(t *testing.T) { + executor, _ := NewGrpcExecutor(nil, DefaultLogger()) + g := executor.(*grpcExecutor) + + req := &protos.EntityBatchRequest{InstanceId: "@counter@key"} + g.pendingEntities.Store(req.InstanceId, &entityExecutionResult{complete: make(chan struct{})}) + + _, err := g.ExecuteEntity(context.Background(), req) + require.Error(t, err) + assert.Contains(t, err.Error(), "already pending") +} + +func Test_GrpcExecutor_StartInstance_RejectsEntityInstanceID(t *testing.T) { + executor, _ := NewGrpcExecutor(nil, DefaultLogger()) + g := executor.(*grpcExecutor) + + _, err := g.StartInstance(context.Background(), &protos.CreateInstanceRequest{ + Name: "orchestrator", + InstanceId: "@counter@key", + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "reserved entity format") +} + +func Test_GrpcExecutor_SignalEntity_PreservesScheduledTimeAndRequestID(t *testing.T) { + be := &capturingBackend{} + executor, _ := NewGrpcExecutor(be, DefaultLogger()) + g := executor.(*grpcExecutor) + + scheduledTime := time.Now().Add(2 * time.Hour).UTC().Truncate(time.Millisecond) + _, err := g.SignalEntity(context.Background(), &protos.SignalEntityRequest{ + InstanceId: "@counter@key", + Name: "increment", + RequestId: "request-123", + ScheduledTime: timestamppb.New(scheduledTime), + }) + require.NoError(t, err) + + require.Len(t, be.created, 1) + require.Len(t, be.added, 1) + require.Equal(t, api.InstanceID("@counter@key"), be.addedTo[0]) + require.WithinDuration(t, scheduledTime, be.added[0].Timestamp.AsTime(), time.Millisecond) + + payload := be.added[0].GetEventRaised().GetInput().GetValue() + var msg helpers.EntityRequestMessage + require.NoError(t, json.Unmarshal([]byte(payload), &msg)) + require.Equal(t, "request-123", msg.ID) + require.True(t, msg.IsSignal) + require.Equal(t, "increment", msg.Operation) +} + +func Test_GrpcExecutor_CompleteEntityTask_RemovesMetadataCorrelatedQueueEntry(t *testing.T) { + executor, _ := NewGrpcExecutor(nil, DefaultLogger()) + g := executor.(*grpcExecutor) + + first := &entityExecutionResult{complete: make(chan struct{})} + second := &entityExecutionResult{complete: make(chan struct{})} + g.pendingEntities.Store("@counter@one", first) + g.pendingEntities.Store("@counter@two", second) + g.entityQueue.Enqueue("@counter@one") + g.entityQueue.Enqueue("@counter@two") + + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("entity-instance-id", "@counter@one")) + _, err := g.CompleteEntityTask(ctx, &protos.EntityBatchResult{}) + require.NoError(t, err) + + next, ok := g.entityQueue.Dequeue() + require.True(t, ok) + assert.Equal(t, "@counter@two", next) + + _, ok = g.entityQueue.Dequeue() + assert.False(t, ok) +} diff --git a/backend/orchestration.go b/backend/orchestration.go index 567108c5..eeee13ce 100644 --- a/backend/orchestration.go +++ b/backend/orchestration.go @@ -2,10 +2,14 @@ package backend import ( "context" + "encoding/hex" + "encoding/json" "errors" "fmt" + "strings" "time" + "github.com/google/uuid" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" @@ -24,10 +28,19 @@ type OrchestratorExecutor interface { newEvents []*protos.HistoryEvent) (*ExecutionResults, error) } +// EntityExecutor is an optional extension of [Executor] that adds entity execution support. +// If the executor passed to [NewOrchestrationWorker] implements this interface, +// entity work items will be automatically dispatched. +type EntityExecutor interface { + Executor + ExecuteEntity(context.Context, *protos.EntityBatchRequest) (*protos.EntityBatchResult, error) +} + type orchestratorProcessor struct { - be Backend - executor OrchestratorExecutor - logger Logger + be Backend + executor OrchestratorExecutor + entityExecutor EntityExecutor + logger Logger } func NewOrchestrationWorker(be Backend, executor OrchestratorExecutor, logger Logger, opts ...NewTaskWorkerOptions) TaskWorker { @@ -36,6 +49,10 @@ func NewOrchestrationWorker(be Backend, executor OrchestratorExecutor, logger Lo executor: executor, logger: logger, } + // If the executor also implements EntityExecutor, use it for entity dispatch + if ee, ok := executor.(EntityExecutor); ok { + processor.entityExecutor = ee + } return NewTaskWorker(processor, logger, opts...) } @@ -54,6 +71,13 @@ func (w *orchestratorProcessor) ProcessWorkItem(ctx context.Context, cwi WorkIte wi := cwi.(*OrchestrationWorkItem) w.logger.Debugf("%v: received work item with %d new event(s): %v", wi.InstanceID, len(wi.NewEvents), helpers.HistoryListSummary(wi.NewEvents)) + // Detect entity instances by their "@name@key" prefix and route to entity executor + if w.entityExecutor != nil { + if _, err := api.EntityIDFromString(string(wi.InstanceID)); err == nil { + return w.processEntityWorkItem(ctx, wi) + } + } + // TODO: Caching // In the fullness of time, we should consider caching executors and runtime state // so that we can skip the loading of state and/or the creation of executors. A cached @@ -133,6 +157,27 @@ func (w *orchestratorProcessor) ProcessWorkItem(ctx context.Context, cwi WorkIte // CompleteWorkItem implements TaskProcessor func (p *orchestratorProcessor) CompleteWorkItem(ctx context.Context, wi WorkItem) error { owi := wi.(*OrchestrationWorkItem) + + // Auto-create entity instances for any pending messages targeting entity IDs. + // This ensures CallEntity from orchestrations works even when the target entity + // doesn't exist yet, without requiring backend-specific entity support. + for _, msg := range owi.State.PendingMessages() { + if msg.HistoryEvent.GetExecutionStarted() != nil { + continue // sub-orchestration creation, handled by the backend + } + entityID, err := api.EntityIDFromString(msg.TargetInstanceID) + if err != nil { + continue // not an entity ID + } + startEvent := helpers.NewExecutionStartedEvent(entityID.Name, msg.TargetInstanceID, nil, nil, nil, nil) + if createErr := p.be.CreateOrchestrationInstance(ctx, startEvent, WithOrchestrationIdReusePolicy(&protos.OrchestrationIdReusePolicy{ + Action: protos.CreateOrchestrationAction_IGNORE, + OperationStatus: []protos.OrchestrationStatus{protos.OrchestrationStatus_ORCHESTRATION_STATUS_RUNNING}, + })); createErr != nil && !errors.Is(createErr, api.ErrDuplicateInstance) && !errors.Is(createErr, api.ErrIgnoreInstance) { + p.logger.Warnf("%v: failed to auto-create entity instance %s: %v", owi.InstanceID, msg.TargetInstanceID, createErr) + } + } + return p.be.CompleteOrchestrationWorkItem(ctx, owi) } @@ -142,6 +187,214 @@ func (p *orchestratorProcessor) AbandonWorkItem(ctx context.Context, wi WorkItem return p.be.AbandonOrchestrationWorkItem(ctx, owi) } +// processEntityWorkItem handles orchestration work items that represent entity instances. +// Entity instances are identified by their "@name@key" instance ID format. +// This method converts incoming orchestration events into entity operations, +// executes them via the EntityExecutor, and writes the results back as orchestration state. +func (w *orchestratorProcessor) processEntityWorkItem(ctx context.Context, wi *OrchestrationWorkItem) error { + iid := string(wi.InstanceID) + w.logger.Debugf("%v: processing as entity work item", wi.InstanceID) + + // Load existing state if needed + if wi.State == nil { + state, err := w.be.GetOrchestrationRuntimeState(ctx, wi) + if err != nil { + return fmt.Errorf("failed to load entity state: %w", err) + } + wi.State = state + } + + // Extract entity state from the orchestration metadata (stored as CustomStatus) + var entityState *wrapperspb.StringValue + meta, err := w.be.GetOrchestrationMetadata(ctx, wi.InstanceID) + if err == nil && meta != nil && meta.SerializedCustomStatus != "" { + entityState = wrapperspb.String(meta.SerializedCustomStatus) + } + + // entityCallInfo tracks response routing for CallEntity requests. + type entityCallInfo struct { + callerInstanceID string + requestID string + isSignal bool + } + + // Convert new EventRaised and EventSent events into entity OperationRequests. + // Events use the .NET-compatible protocol: event name is "op" and the payload + // is a JSON EntityRequestMessage containing routing and operation information. + var operations []*protos.OperationRequest + var callInfos []entityCallInfo // parallel array + for _, e := range wi.NewEvents { + var eventName string + var eventInput *wrapperspb.StringValue + + if er := e.GetEventRaised(); er != nil { + eventName = er.Name + eventInput = er.Input + } else if es := e.GetEventSent(); es != nil { + eventName = es.Name + eventInput = es.Input + } else { + continue + } + + if !strings.EqualFold(eventName, helpers.EntityRequestEventName) { + continue + } + + var reqMsg helpers.EntityRequestMessage + if eventInput == nil || eventInput.GetValue() == "" { + w.logger.Warnf("%v: received 'op' event with no payload, skipping", wi.InstanceID) + continue + } + if err := json.Unmarshal([]byte(eventInput.GetValue()), &reqMsg); err != nil { + w.logger.Warnf("%v: failed to parse RequestMessage: %v", wi.InstanceID, err) + continue + } + + var inputVal *wrapperspb.StringValue + if reqMsg.Input != "" { + inputVal = wrapperspb.String(reqMsg.Input) + } + + operations = append(operations, &protos.OperationRequest{ + Operation: reqMsg.Operation, + RequestId: reqMsg.ID, + Input: inputVal, + }) + callInfos = append(callInfos, entityCallInfo{ + callerInstanceID: reqMsg.ParentInstanceID, + requestID: reqMsg.ID, + isSignal: reqMsg.IsSignal, + }) + } + + // Ensure the entity orchestration instance exists in state + if wi.State.startEvent == nil { + entityID, err := api.EntityIDFromString(iid) + if err != nil { + return fmt.Errorf("invalid entity instance ID format: %w", err) + } + startEvent := helpers.NewExecutionStartedEvent(entityID.Name, iid, nil, nil, nil, nil) + if err := wi.State.AddEvent(helpers.NewOrchestratorStartedEvent()); err != nil { + return fmt.Errorf("failed to add orchestrator started event: %w", err) + } + if err := wi.State.AddEvent(startEvent); err != nil { + return fmt.Errorf("failed to initialize entity state: %w", err) + } + } + + // Add incoming events to state history + for _, e := range wi.NewEvents { + if err := wi.State.AddEvent(e); err != nil { + if !errors.Is(err, ErrDuplicateEvent) { + return fmt.Errorf("failed to add event to entity state: %w", err) + } + w.logger.Debugf("%v: skipping duplicate event in entity history", wi.InstanceID) + } + } + + if len(operations) == 0 { + w.logger.Debugf("%v: no entity operations to process", wi.InstanceID) + return nil + } + + // Build and execute the entity batch + batchReq := &protos.EntityBatchRequest{ + InstanceId: iid, + EntityState: entityState, + Operations: operations, + } + + batchResult, err := w.entityExecutor.ExecuteEntity(ctx, batchReq) + if err != nil { + return fmt.Errorf("failed to execute entity: %w", err) + } + if batchResult.FailureDetails != nil { + w.logger.Errorf("%v: non-retriable entity execution failure: %s", wi.InstanceID, batchResult.FailureDetails.ErrorMessage) + return nil + } + + // Save entity state as the orchestration's custom status + wi.State.CustomStatus = batchResult.EntityState + + // Send results back to calling orchestrations (for CallEntity requests) + for i, info := range callInfos { + if info.isSignal || info.callerInstanceID == "" || info.requestID == "" { + continue // signal, no response needed + } + if i >= len(batchResult.Results) { + break + } + + // Build the .NET-compatible EntityResponseMessage payload. + var resp helpers.EntityResponseMessage + if success := batchResult.Results[i].GetSuccess(); success != nil { + if success.Result != nil { + resp.Result = success.Result.GetValue() + } + } else if failure := batchResult.Results[i].GetFailure(); failure != nil { + resp.ErrorMessage = failure.FailureDetails.GetErrorMessage() + } + + respJSON, err := json.Marshal(resp) + if err != nil { + w.logger.Warnf("%v: failed to marshal entity response: %v", wi.InstanceID, err) + continue + } + + // Send the result as an EventRaised to the caller orchestration, using the requestID + // as the event name so it matches the WaitForSingleEvent in CallEntity. + responseEvent := helpers.NewEventRaisedEvent(info.requestID, wrapperspb.String(string(respJSON))) + if err := w.be.AddNewOrchestrationEvent(ctx, api.InstanceID(info.callerInstanceID), responseEvent); err != nil { + w.logger.Warnf("%v: failed to send entity response to %s: %v", wi.InstanceID, info.callerInstanceID, err) + } + } + + // Process actions from the entity batch result (signals to other entities, new orchestrations) + for _, action := range batchResult.Actions { + if signal := action.GetSendSignal(); signal != nil { + // Wrap entity-to-entity signals in the .NET-compatible EntityRequestMessage format. + sigMsg := helpers.EntityRequestMessage{ + ID: uuid.New().String(), + IsSignal: true, + Operation: signal.Name, + } + if signal.Input != nil { + sigMsg.Input = signal.Input.GetValue() + } + sigJSON, err := json.Marshal(sigMsg) + if err != nil { + w.logger.Warnf("%v: failed to marshal signal request: %v", wi.InstanceID, err) + continue + } + e := helpers.NewEventRaisedEvent(helpers.EntityRequestEventName, wrapperspb.String(string(sigJSON))) + if signal.ScheduledTime != nil { + e.Timestamp = signal.ScheduledTime + } + if err := w.be.AddNewOrchestrationEvent(ctx, api.InstanceID(signal.InstanceId), e); err != nil { + w.logger.Warnf("%v: failed to send entity signal to %s: %v", wi.InstanceID, signal.InstanceId, err) + } + } else if startOrch := action.GetStartNewOrchestration(); startOrch != nil { + orchInstanceID := startOrch.InstanceId + if orchInstanceID == "" { + id := uuid.New() + orchInstanceID = hex.EncodeToString(id[:]) + } + if err := helpers.ValidateOrchestrationInstanceID(orchInstanceID); err != nil { + w.logger.Warnf("%v: dropping entity start-orchestration action for reserved instance ID %q: %v", wi.InstanceID, orchInstanceID, err) + continue + } + e := helpers.NewExecutionStartedEvent(startOrch.Name, orchInstanceID, startOrch.Input, nil, nil, startOrch.ScheduledTime) + if err := w.be.CreateOrchestrationInstance(ctx, e); err != nil { + w.logger.Warnf("%v: failed to start orchestration %s: %v", wi.InstanceID, orchInstanceID, err) + } + } + } + + w.logger.Debugf("%v: entity processed %d operation(s)", wi.InstanceID, len(operations)) + return nil +} + func (w *orchestratorProcessor) applyWorkItem(ctx context.Context, wi *OrchestrationWorkItem) (context.Context, trace.Span, bool) { // Ignore work items for orchestrations that are completed or are in a corrupted state. switch { diff --git a/backend/postgres/postgres.go b/backend/postgres/postgres.go index 10d83363..394f1efd 100644 --- a/backend/postgres/postgres.go +++ b/backend/postgres/postgres.go @@ -140,7 +140,7 @@ func (be *postgresBackend) AbandonOrchestrationWorkItem(ctx context.Context, wi } defer tx.Rollback(ctx) //nolint:errcheck // rollback after commit is a no-op - var visibleTime*time.Time = nil + var visibleTime *time.Time = nil if delay := wi.GetAbandonDelay(); delay > 0 { t := time.Now().UTC().Add(delay) visibleTime = &t @@ -643,11 +643,13 @@ func (be *postgresBackend) AddNewOrchestrationEvent(ctx context.Context, iid api return err } + visibleTime := helpers.GetVisibleTime(e) _, err = be.db.Exec( ctx, - `INSERT INTO NewEvents (InstanceID, EventPayload) VALUES ($1, $2)`, + `INSERT INTO NewEvents (InstanceID, EventPayload, VisibleTime) VALUES ($1, $2, $3)`, string(iid), eventPayload, + visibleTime, ) if err != nil { @@ -769,7 +771,7 @@ func (be *postgresBackend) GetOrchestrationWorkItem(ctx context.Context) (*backe defer tx.Rollback(ctx) //nolint:errcheck // rollback after commit is a no-op now := time.Now().UTC() - newLockExpiration:= now.Add(be.options.OrchestrationLockTimeout) + newLockExpiration := now.Add(be.options.OrchestrationLockTimeout) // Place a lock on an orchestration instance that has new events that are ready to be executed. row := tx.QueryRow( diff --git a/backend/sqlite/sqlite.go b/backend/sqlite/sqlite.go index 1e1d1eab..583081df 100644 --- a/backend/sqlite/sqlite.go +++ b/backend/sqlite/sqlite.go @@ -620,11 +620,13 @@ func (be *sqliteBackend) AddNewOrchestrationEvent(ctx context.Context, iid api.I return err } + visibleTime := helpers.GetVisibleTime(e) _, err = be.db.ExecContext( ctx, - `INSERT INTO NewEvents ([InstanceID], [EventPayload]) VALUES (?, ?)`, + `INSERT INTO NewEvents ([InstanceID], [EventPayload], [VisibleTime]) VALUES (?, ?, ?)`, string(iid), eventPayload, + visibleTime, ) if err != nil { diff --git a/client/client_grpc.go b/client/client_grpc.go index 996fd985..cd8f7a8d 100644 --- a/client/client_grpc.go +++ b/client/client_grpc.go @@ -7,10 +7,12 @@ import ( "github.com/cenkalti/backoff/v4" "github.com/google/uuid" "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/wrapperspb" "github.com/microsoft/durabletask-go/api" "github.com/microsoft/durabletask-go/backend" + "github.com/microsoft/durabletask-go/internal/helpers" "github.com/microsoft/durabletask-go/internal/protos" ) @@ -41,7 +43,9 @@ func (c *TaskHubGrpcClient) ScheduleNewOrchestration(ctx context.Context, orches if req.InstanceId == "" { req.InstanceId = uuid.NewString() } - + if err := helpers.ValidateOrchestrationInstanceID(req.InstanceId); err != nil { + return api.EmptyInstanceID, err + } resp, err := c.client.StartInstance(ctx, req) if err != nil { if ctx.Err() != nil { @@ -212,6 +216,146 @@ func (c *TaskHubGrpcClient) PurgeOrchestrationState(ctx context.Context, id api. return nil } +// SignalEntity sends a fire-and-forget signal to an entity, triggering the specified operation. +// +// If the entity doesn't exist, it will be created automatically when the signal is processed. +func (c *TaskHubGrpcClient) SignalEntity(ctx context.Context, entityID api.EntityID, operationName string, opts ...api.SignalEntityOptions) error { + if err := helpers.ValidateEntityName(entityID.Name); err != nil { + return err + } + + req := &protos.SignalEntityRequest{ + InstanceId: entityID.String(), + Name: operationName, + } + for _, configure := range opts { + if err := configure(req); err != nil { + return fmt.Errorf("failed to configure signal entity request: %w", err) + } + } + + if _, err := c.client.SignalEntity(ctx, req); err != nil { + if ctx.Err() != nil { + return ctx.Err() + } + return fmt.Errorf("failed to signal entity: %w", err) + } + return nil +} + +// FetchEntityMetadata retrieves metadata about an entity instance. +// +// Returns nil if the entity doesn't exist. +func (c *TaskHubGrpcClient) FetchEntityMetadata(ctx context.Context, entityID api.EntityID, includeState bool) (*api.EntityMetadata, error) { + if err := helpers.ValidateEntityName(entityID.Name); err != nil { + return nil, err + } + + req := &protos.GetEntityRequest{ + InstanceId: entityID.String(), + IncludeState: includeState, + } + + resp, err := c.client.GetEntity(ctx, req) + if err != nil { + if ctx.Err() != nil { + return nil, ctx.Err() + } + return nil, fmt.Errorf("failed to get entity metadata: %w", err) + } + if !resp.Exists || resp.Entity == nil { + return nil, nil + } + + result := &api.EntityMetadata{ + InstanceID: entityID, + BacklogQueueSize: resp.Entity.BacklogQueueSize, + LockedBy: resp.Entity.LockedBy.GetValue(), + SerializedState: resp.Entity.SerializedState.GetValue(), + } + if resp.Entity.LastModifiedTime != nil { + result.LastModifiedTime = resp.Entity.LastModifiedTime.AsTime() + } + return result, nil +} + +// QueryEntities queries entities matching the specified filter criteria. +func (c *TaskHubGrpcClient) QueryEntities(ctx context.Context, query api.EntityQuery) (*api.EntityQueryResults, error) { + protoQuery := &protos.EntityQuery{ + IncludeState: query.IncludeState, + IncludeTransient: query.IncludeTransient, + } + if query.InstanceIDStartsWith != "" { + protoQuery.InstanceIdStartsWith = wrapperspb.String(query.InstanceIDStartsWith) + } + if !query.LastModifiedFrom.IsZero() { + protoQuery.LastModifiedFrom = timestamppb.New(query.LastModifiedFrom) + } + if !query.LastModifiedTo.IsZero() { + protoQuery.LastModifiedTo = timestamppb.New(query.LastModifiedTo) + } + if query.PageSize > 0 { + protoQuery.PageSize = wrapperspb.Int32(query.PageSize) + } + if query.ContinuationToken != "" { + protoQuery.ContinuationToken = wrapperspb.String(query.ContinuationToken) + } + + resp, err := c.client.QueryEntities(ctx, &protos.QueryEntitiesRequest{Query: protoQuery}) + if err != nil { + if ctx.Err() != nil { + return nil, ctx.Err() + } + return nil, fmt.Errorf("failed to query entities: %w", err) + } + + results := &api.EntityQueryResults{ + ContinuationToken: resp.ContinuationToken.GetValue(), + } + for _, e := range resp.Entities { + entityID, parseErr := api.EntityIDFromString(e.InstanceId) + if parseErr != nil { + return nil, fmt.Errorf("failed to parse entity ID %q: %w", e.InstanceId, parseErr) + } + meta := &api.EntityMetadata{ + InstanceID: entityID, + BacklogQueueSize: e.BacklogQueueSize, + LockedBy: e.LockedBy.GetValue(), + SerializedState: e.SerializedState.GetValue(), + } + if e.LastModifiedTime != nil { + meta.LastModifiedTime = e.LastModifiedTime.AsTime() + } + results.Entities = append(results.Entities, meta) + } + return results, nil +} + +// CleanEntityStorage performs garbage collection on entity storage. +func (c *TaskHubGrpcClient) CleanEntityStorage(ctx context.Context, req api.CleanEntityStorageRequest) (*api.CleanEntityStorageResult, error) { + protoReq := &protos.CleanEntityStorageRequest{ + RemoveEmptyEntities: req.RemoveEmptyEntities, + ReleaseOrphanedLocks: req.ReleaseOrphanedLocks, + } + if req.ContinuationToken != "" { + protoReq.ContinuationToken = wrapperspb.String(req.ContinuationToken) + } + + resp, err := c.client.CleanEntityStorage(ctx, protoReq) + if err != nil { + if ctx.Err() != nil { + return nil, ctx.Err() + } + return nil, fmt.Errorf("failed to clean entity storage: %w", err) + } + + return &api.CleanEntityStorageResult{ + EmptyEntitiesRemoved: resp.EmptyEntitiesRemoved, + OrphanedLocksReleased: resp.OrphanedLocksReleased, + ContinuationToken: resp.ContinuationToken.GetValue(), + }, nil +} + func makeGetInstanceRequest(id api.InstanceID, opts []api.FetchOrchestrationMetadataOptions) *protos.GetInstanceRequest { req := &protos.GetInstanceRequest{ InstanceId: string(id), diff --git a/client/client_grpc_test.go b/client/client_grpc_test.go new file mode 100644 index 00000000..96137f71 --- /dev/null +++ b/client/client_grpc_test.go @@ -0,0 +1,48 @@ +package client + +import ( + "context" + "fmt" + "testing" + + "github.com/microsoft/durabletask-go/api" + "github.com/microsoft/durabletask-go/backend" + "github.com/microsoft/durabletask-go/internal/protos" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" +) + +type fakeClientConn struct { + invoke func(ctx context.Context, method string, args any, reply any, opts ...grpc.CallOption) error +} + +func (f fakeClientConn) Invoke(ctx context.Context, method string, args any, reply any, opts ...grpc.CallOption) error { + return f.invoke(ctx, method, args, reply, opts...) +} + +func (fakeClientConn) NewStream(context.Context, *grpc.StreamDesc, string, ...grpc.CallOption) (grpc.ClientStream, error) { + return nil, fmt.Errorf("unexpected stream call") +} + +func Test_TaskHubGrpcClient_QueryEntities_ErrorsOnInvalidEntityID(t *testing.T) { + conn := fakeClientConn{ + invoke: func(ctx context.Context, method string, args any, reply any, opts ...grpc.CallOption) error { + if method != protos.TaskHubSidecarService_QueryEntities_FullMethodName { + return fmt.Errorf("unexpected method: %s", method) + } + + resp := reply.(*protos.QueryEntitiesResponse) + resp.Entities = []*protos.EntityMetadata{ + {InstanceId: "not-an-entity-id"}, + } + return nil + }, + } + + client := NewTaskHubGrpcClient(conn, backend.DefaultLogger()) + + _, err := client.QueryEntities(context.Background(), api.EntityQuery{}) + require.Error(t, err) + assert.Contains(t, err.Error(), `failed to parse entity ID "not-an-entity-id"`) +} diff --git a/client/worker_grpc.go b/client/worker_grpc.go index 69c1bea0..24a6997b 100644 --- a/client/worker_grpc.go +++ b/client/worker_grpc.go @@ -14,6 +14,7 @@ import ( "github.com/microsoft/durabletask-go/internal/protos" "github.com/microsoft/durabletask-go/task" "google.golang.org/grpc/codes" + grpcmetadata "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/wrapperspb" @@ -121,6 +122,8 @@ func (c *TaskHubGrpcClient) StartWorkItemListener(ctx context.Context, r *task.T go c.processOrchestrationWorkItem(ctx, executor, orchReq) } else if actReq := workItem.GetActivityRequest(); actReq != nil { go c.processActivityWorkItem(ctx, executor, actReq) + } else if entityReq := workItem.GetEntityRequest(); entityReq != nil { + go c.processEntityWorkItem(ctx, executor, entityReq) } else { c.logger.Warnf("received unknown work item type: %v", workItem) } @@ -201,6 +204,41 @@ func (c *TaskHubGrpcClient) processActivityWorkItem( } } +func (c *TaskHubGrpcClient) processEntityWorkItem( + ctx context.Context, + executor backend.Executor, + req *protos.EntityBatchRequest, +) { + ee, ok := executor.(backend.EntityExecutor) + if !ok { + c.logger.Errorf("executor does not support entity execution") + return + } + + result, err := ee.ExecuteEntity(ctx, req) + + if err != nil { + result = &protos.EntityBatchResult{ + FailureDetails: &protos.TaskFailureDetails{ + ErrorType: fmt.Sprintf("%T", err), + ErrorMessage: err.Error(), + }, + } + } + + // Pass the entity instance ID via gRPC metadata so the server can correlate + // the completion with the correct pending entity (EntityBatchResult doesn't + // include an instance ID field in the proto). + completeCtx := grpcmetadata.AppendToOutgoingContext(ctx, "entity-instance-id", req.InstanceId) + if _, err = c.client.CompleteEntityTask(completeCtx, result); err != nil { + if ctx.Err() != nil { + c.logger.Warn("failed to complete entity task: context canceled") + } else { + c.logger.Errorf("failed to complete entity task: %v", err) + } + } +} + func newInfiniteRetries() *backoff.ExponentialBackOff { b := backoff.NewExponentialBackOff() // max wait of 15 seconds between retries diff --git a/internal/helpers/entity.go b/internal/helpers/entity.go new file mode 100644 index 00000000..f7e99e7b --- /dev/null +++ b/internal/helpers/entity.go @@ -0,0 +1,22 @@ +package helpers + +// EntityRequestEventName is the event name used for all entity operation requests. +// This matches the .NET DTFx EntityMessageEventNames.RequestMessageEventName constant. +const EntityRequestEventName = "op" + +// EntityRequestMessage is the payload sent to an entity for operation requests. +// This matches the .NET DTFx RequestMessage format for wire compatibility. +type EntityRequestMessage struct { + ID string `json:"id"` + ParentInstanceID string `json:"parentInstanceId,omitempty"` + IsSignal bool `json:"isSignal"` + Operation string `json:"operation"` + Input string `json:"input,omitempty"` +} + +// EntityResponseMessage is the payload sent back from an entity to a calling orchestration. +// This matches the .NET DTFx ResponseMessage format for wire compatibility. +type EntityResponseMessage struct { + Result string `json:"result,omitempty"` + ErrorMessage string `json:"errorMessage,omitempty"` +} diff --git a/internal/helpers/entity_ids.go b/internal/helpers/entity_ids.go new file mode 100644 index 00000000..f5feb47b --- /dev/null +++ b/internal/helpers/entity_ids.go @@ -0,0 +1,52 @@ +package helpers + +import ( + "fmt" + "strings" +) + +// ValidateEntityName applies the durable-entity naming constraints used by the wire format. +func ValidateEntityName(name string) error { + switch { + case name == "": + return fmt.Errorf("invalid entity id: entity name must not be empty") + case strings.Contains(name, "@"): + return fmt.Errorf("invalid entity id: entity name %q must not contain '@'", name) + default: + return nil + } +} + +// ParseEntityInstanceID parses an entity instance ID in the format "@@". +func ParseEntityInstanceID(instanceID string) (string, string, error) { + if !strings.HasPrefix(instanceID, "@") { + return "", "", fmt.Errorf("invalid entity instance ID format: %q", instanceID) + } + + trimmed := instanceID[1:] + name, key, ok := strings.Cut(trimmed, "@") + if !ok { + return "", "", fmt.Errorf("invalid entity instance ID format: missing second '@'") + } + if err := ValidateEntityName(name); err != nil { + return "", "", err + } + return strings.ToLower(name), key, nil +} + +// IsEntityInstanceID reports whether the instance ID uses the reserved entity format. +func IsEntityInstanceID(instanceID string) bool { + _, _, err := ParseEntityInstanceID(instanceID) + return err == nil +} + +// ValidateOrchestrationInstanceID rejects orchestration IDs that collide with entity instance IDs. +func ValidateOrchestrationInstanceID(instanceID string) error { + if instanceID == "" { + return nil + } + if IsEntityInstanceID(instanceID) { + return fmt.Errorf("orchestration instance ID %q uses the reserved entity format", instanceID) + } + return nil +} diff --git a/internal/helpers/visibility.go b/internal/helpers/visibility.go new file mode 100644 index 00000000..2ac20f70 --- /dev/null +++ b/internal/helpers/visibility.go @@ -0,0 +1,21 @@ +package helpers + +import ( + "time" + + "github.com/microsoft/durabletask-go/internal/protos" +) + +// GetVisibleTime returns the visibility time for a new event when it should be delayed. +func GetVisibleTime(e *protos.HistoryEvent) *time.Time { + if e == nil || e.Timestamp == nil { + return nil + } + + visibleTime := e.Timestamp.AsTime() + if !visibleTime.After(time.Now()) { + return nil + } + + return &visibleTime +} diff --git a/internal/protos/orchestrator_service.pb.go b/internal/protos/orchestrator_service.pb.go index 52eb6d21..68a89204 100644 --- a/internal/protos/orchestrator_service.pb.go +++ b/internal/protos/orchestrator_service.pb.go @@ -3,19 +3,19 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 -// protoc v3.12.4 +// protoc-gen-go v1.30.0 +// protoc v7.34.0 // source: orchestrator_service.proto package protos import ( - duration "google.golang.org/protobuf/types/known/durationpb" - empty "google.golang.org/protobuf/types/known/emptypb" - timestamp "google.golang.org/protobuf/types/known/timestamppb" - wrappers "google.golang.org/protobuf/types/known/wrapperspb" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + emptypb "google.golang.org/protobuf/types/known/emptypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" reflect "reflect" sync "sync" ) @@ -145,8 +145,8 @@ type OrchestrationInstance struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - ExecutionId *wrappers.StringValue `protobuf:"bytes,2,opt,name=executionId,proto3" json:"executionId,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + ExecutionId *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=executionId,proto3" json:"executionId,omitempty"` } func (x *OrchestrationInstance) Reset() { @@ -188,7 +188,7 @@ func (x *OrchestrationInstance) GetInstanceId() string { return "" } -func (x *OrchestrationInstance) GetExecutionId() *wrappers.StringValue { +func (x *OrchestrationInstance) GetExecutionId() *wrapperspb.StringValue { if x != nil { return x.ExecutionId } @@ -200,11 +200,11 @@ type ActivityRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Version *wrappers.StringValue `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` - Input *wrappers.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` - OrchestrationInstance *OrchestrationInstance `protobuf:"bytes,4,opt,name=orchestrationInstance,proto3" json:"orchestrationInstance,omitempty"` - TaskId int32 `protobuf:"varint,5,opt,name=taskId,proto3" json:"taskId,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Version *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` + OrchestrationInstance *OrchestrationInstance `protobuf:"bytes,4,opt,name=orchestrationInstance,proto3" json:"orchestrationInstance,omitempty"` + TaskId int32 `protobuf:"varint,5,opt,name=taskId,proto3" json:"taskId,omitempty"` } func (x *ActivityRequest) Reset() { @@ -246,14 +246,14 @@ func (x *ActivityRequest) GetName() string { return "" } -func (x *ActivityRequest) GetVersion() *wrappers.StringValue { +func (x *ActivityRequest) GetVersion() *wrapperspb.StringValue { if x != nil { return x.Version } return nil } -func (x *ActivityRequest) GetInput() *wrappers.StringValue { +func (x *ActivityRequest) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } @@ -279,10 +279,10 @@ type ActivityResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - TaskId int32 `protobuf:"varint,2,opt,name=taskId,proto3" json:"taskId,omitempty"` - Result *wrappers.StringValue `protobuf:"bytes,3,opt,name=result,proto3" json:"result,omitempty"` - FailureDetails *TaskFailureDetails `protobuf:"bytes,4,opt,name=failureDetails,proto3" json:"failureDetails,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + TaskId int32 `protobuf:"varint,2,opt,name=taskId,proto3" json:"taskId,omitempty"` + Result *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=result,proto3" json:"result,omitempty"` + FailureDetails *TaskFailureDetails `protobuf:"bytes,4,opt,name=failureDetails,proto3" json:"failureDetails,omitempty"` } func (x *ActivityResponse) Reset() { @@ -331,7 +331,7 @@ func (x *ActivityResponse) GetTaskId() int32 { return 0 } -func (x *ActivityResponse) GetResult() *wrappers.StringValue { +func (x *ActivityResponse) GetResult() *wrapperspb.StringValue { if x != nil { return x.Result } @@ -350,11 +350,11 @@ type TaskFailureDetails struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ErrorType string `protobuf:"bytes,1,opt,name=errorType,proto3" json:"errorType,omitempty"` - ErrorMessage string `protobuf:"bytes,2,opt,name=errorMessage,proto3" json:"errorMessage,omitempty"` - StackTrace *wrappers.StringValue `protobuf:"bytes,3,opt,name=stackTrace,proto3" json:"stackTrace,omitempty"` - InnerFailure *TaskFailureDetails `protobuf:"bytes,4,opt,name=innerFailure,proto3" json:"innerFailure,omitempty"` - IsNonRetriable bool `protobuf:"varint,5,opt,name=isNonRetriable,proto3" json:"isNonRetriable,omitempty"` + ErrorType string `protobuf:"bytes,1,opt,name=errorType,proto3" json:"errorType,omitempty"` + ErrorMessage string `protobuf:"bytes,2,opt,name=errorMessage,proto3" json:"errorMessage,omitempty"` + StackTrace *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=stackTrace,proto3" json:"stackTrace,omitempty"` + InnerFailure *TaskFailureDetails `protobuf:"bytes,4,opt,name=innerFailure,proto3" json:"innerFailure,omitempty"` + IsNonRetriable bool `protobuf:"varint,5,opt,name=isNonRetriable,proto3" json:"isNonRetriable,omitempty"` } func (x *TaskFailureDetails) Reset() { @@ -403,7 +403,7 @@ func (x *TaskFailureDetails) GetErrorMessage() string { return "" } -func (x *TaskFailureDetails) GetStackTrace() *wrappers.StringValue { +func (x *TaskFailureDetails) GetStackTrace() *wrapperspb.StringValue { if x != nil { return x.StackTrace } @@ -429,10 +429,10 @@ type ParentInstanceInfo struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - TaskScheduledId int32 `protobuf:"varint,1,opt,name=taskScheduledId,proto3" json:"taskScheduledId,omitempty"` - Name *wrappers.StringValue `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Version *wrappers.StringValue `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` - OrchestrationInstance *OrchestrationInstance `protobuf:"bytes,4,opt,name=orchestrationInstance,proto3" json:"orchestrationInstance,omitempty"` + TaskScheduledId int32 `protobuf:"varint,1,opt,name=taskScheduledId,proto3" json:"taskScheduledId,omitempty"` + Name *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Version *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + OrchestrationInstance *OrchestrationInstance `protobuf:"bytes,4,opt,name=orchestrationInstance,proto3" json:"orchestrationInstance,omitempty"` } func (x *ParentInstanceInfo) Reset() { @@ -474,14 +474,14 @@ func (x *ParentInstanceInfo) GetTaskScheduledId() int32 { return 0 } -func (x *ParentInstanceInfo) GetName() *wrappers.StringValue { +func (x *ParentInstanceInfo) GetName() *wrapperspb.StringValue { if x != nil { return x.Name } return nil } -func (x *ParentInstanceInfo) GetVersion() *wrappers.StringValue { +func (x *ParentInstanceInfo) GetVersion() *wrapperspb.StringValue { if x != nil { return x.Version } @@ -502,8 +502,8 @@ type TraceContext struct { TraceParent string `protobuf:"bytes,1,opt,name=traceParent,proto3" json:"traceParent,omitempty"` // Deprecated: Marked as deprecated in orchestrator_service.proto. - SpanID string `protobuf:"bytes,2,opt,name=spanID,proto3" json:"spanID,omitempty"` - TraceState *wrappers.StringValue `protobuf:"bytes,3,opt,name=traceState,proto3" json:"traceState,omitempty"` + SpanID string `protobuf:"bytes,2,opt,name=spanID,proto3" json:"spanID,omitempty"` + TraceState *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=traceState,proto3" json:"traceState,omitempty"` } func (x *TraceContext) Reset() { @@ -553,7 +553,7 @@ func (x *TraceContext) GetSpanID() string { return "" } -func (x *TraceContext) GetTraceState() *wrappers.StringValue { +func (x *TraceContext) GetTraceState() *wrapperspb.StringValue { if x != nil { return x.TraceState } @@ -565,14 +565,14 @@ type ExecutionStartedEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Version *wrappers.StringValue `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` - Input *wrappers.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` - OrchestrationInstance *OrchestrationInstance `protobuf:"bytes,4,opt,name=orchestrationInstance,proto3" json:"orchestrationInstance,omitempty"` - ParentInstance *ParentInstanceInfo `protobuf:"bytes,5,opt,name=parentInstance,proto3" json:"parentInstance,omitempty"` - ScheduledStartTimestamp *timestamp.Timestamp `protobuf:"bytes,6,opt,name=scheduledStartTimestamp,proto3" json:"scheduledStartTimestamp,omitempty"` - ParentTraceContext *TraceContext `protobuf:"bytes,7,opt,name=parentTraceContext,proto3" json:"parentTraceContext,omitempty"` - OrchestrationSpanID *wrappers.StringValue `protobuf:"bytes,8,opt,name=orchestrationSpanID,proto3" json:"orchestrationSpanID,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Version *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` + OrchestrationInstance *OrchestrationInstance `protobuf:"bytes,4,opt,name=orchestrationInstance,proto3" json:"orchestrationInstance,omitempty"` + ParentInstance *ParentInstanceInfo `protobuf:"bytes,5,opt,name=parentInstance,proto3" json:"parentInstance,omitempty"` + ScheduledStartTimestamp *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=scheduledStartTimestamp,proto3" json:"scheduledStartTimestamp,omitempty"` + ParentTraceContext *TraceContext `protobuf:"bytes,7,opt,name=parentTraceContext,proto3" json:"parentTraceContext,omitempty"` + OrchestrationSpanID *wrapperspb.StringValue `protobuf:"bytes,8,opt,name=orchestrationSpanID,proto3" json:"orchestrationSpanID,omitempty"` } func (x *ExecutionStartedEvent) Reset() { @@ -614,14 +614,14 @@ func (x *ExecutionStartedEvent) GetName() string { return "" } -func (x *ExecutionStartedEvent) GetVersion() *wrappers.StringValue { +func (x *ExecutionStartedEvent) GetVersion() *wrapperspb.StringValue { if x != nil { return x.Version } return nil } -func (x *ExecutionStartedEvent) GetInput() *wrappers.StringValue { +func (x *ExecutionStartedEvent) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } @@ -642,7 +642,7 @@ func (x *ExecutionStartedEvent) GetParentInstance() *ParentInstanceInfo { return nil } -func (x *ExecutionStartedEvent) GetScheduledStartTimestamp() *timestamp.Timestamp { +func (x *ExecutionStartedEvent) GetScheduledStartTimestamp() *timestamppb.Timestamp { if x != nil { return x.ScheduledStartTimestamp } @@ -656,7 +656,7 @@ func (x *ExecutionStartedEvent) GetParentTraceContext() *TraceContext { return nil } -func (x *ExecutionStartedEvent) GetOrchestrationSpanID() *wrappers.StringValue { +func (x *ExecutionStartedEvent) GetOrchestrationSpanID() *wrapperspb.StringValue { if x != nil { return x.OrchestrationSpanID } @@ -668,9 +668,9 @@ type ExecutionCompletedEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - OrchestrationStatus OrchestrationStatus `protobuf:"varint,1,opt,name=orchestrationStatus,proto3,enum=OrchestrationStatus" json:"orchestrationStatus,omitempty"` - Result *wrappers.StringValue `protobuf:"bytes,2,opt,name=result,proto3" json:"result,omitempty"` - FailureDetails *TaskFailureDetails `protobuf:"bytes,3,opt,name=failureDetails,proto3" json:"failureDetails,omitempty"` + OrchestrationStatus OrchestrationStatus `protobuf:"varint,1,opt,name=orchestrationStatus,proto3,enum=OrchestrationStatus" json:"orchestrationStatus,omitempty"` + Result *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=result,proto3" json:"result,omitempty"` + FailureDetails *TaskFailureDetails `protobuf:"bytes,3,opt,name=failureDetails,proto3" json:"failureDetails,omitempty"` } func (x *ExecutionCompletedEvent) Reset() { @@ -712,7 +712,7 @@ func (x *ExecutionCompletedEvent) GetOrchestrationStatus() OrchestrationStatus { return OrchestrationStatus_ORCHESTRATION_STATUS_RUNNING } -func (x *ExecutionCompletedEvent) GetResult() *wrappers.StringValue { +func (x *ExecutionCompletedEvent) GetResult() *wrapperspb.StringValue { if x != nil { return x.Result } @@ -731,8 +731,8 @@ type ExecutionTerminatedEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Input *wrappers.StringValue `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` - Recurse bool `protobuf:"varint,2,opt,name=recurse,proto3" json:"recurse,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` + Recurse bool `protobuf:"varint,2,opt,name=recurse,proto3" json:"recurse,omitempty"` } func (x *ExecutionTerminatedEvent) Reset() { @@ -767,7 +767,7 @@ func (*ExecutionTerminatedEvent) Descriptor() ([]byte, []int) { return file_orchestrator_service_proto_rawDescGZIP(), []int{8} } -func (x *ExecutionTerminatedEvent) GetInput() *wrappers.StringValue { +func (x *ExecutionTerminatedEvent) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } @@ -786,10 +786,10 @@ type TaskScheduledEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Version *wrappers.StringValue `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` - Input *wrappers.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` - ParentTraceContext *TraceContext `protobuf:"bytes,4,opt,name=parentTraceContext,proto3" json:"parentTraceContext,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Version *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` + ParentTraceContext *TraceContext `protobuf:"bytes,4,opt,name=parentTraceContext,proto3" json:"parentTraceContext,omitempty"` } func (x *TaskScheduledEvent) Reset() { @@ -831,14 +831,14 @@ func (x *TaskScheduledEvent) GetName() string { return "" } -func (x *TaskScheduledEvent) GetVersion() *wrappers.StringValue { +func (x *TaskScheduledEvent) GetVersion() *wrapperspb.StringValue { if x != nil { return x.Version } return nil } -func (x *TaskScheduledEvent) GetInput() *wrappers.StringValue { +func (x *TaskScheduledEvent) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } @@ -857,8 +857,8 @@ type TaskCompletedEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - TaskScheduledId int32 `protobuf:"varint,1,opt,name=taskScheduledId,proto3" json:"taskScheduledId,omitempty"` - Result *wrappers.StringValue `protobuf:"bytes,2,opt,name=result,proto3" json:"result,omitempty"` + TaskScheduledId int32 `protobuf:"varint,1,opt,name=taskScheduledId,proto3" json:"taskScheduledId,omitempty"` + Result *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=result,proto3" json:"result,omitempty"` } func (x *TaskCompletedEvent) Reset() { @@ -900,7 +900,7 @@ func (x *TaskCompletedEvent) GetTaskScheduledId() int32 { return 0 } -func (x *TaskCompletedEvent) GetResult() *wrappers.StringValue { +func (x *TaskCompletedEvent) GetResult() *wrapperspb.StringValue { if x != nil { return x.Result } @@ -967,11 +967,11 @@ type SubOrchestrationInstanceCreatedEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Version *wrappers.StringValue `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` - Input *wrappers.StringValue `protobuf:"bytes,4,opt,name=input,proto3" json:"input,omitempty"` - ParentTraceContext *TraceContext `protobuf:"bytes,5,opt,name=parentTraceContext,proto3" json:"parentTraceContext,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Version *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,4,opt,name=input,proto3" json:"input,omitempty"` + ParentTraceContext *TraceContext `protobuf:"bytes,5,opt,name=parentTraceContext,proto3" json:"parentTraceContext,omitempty"` } func (x *SubOrchestrationInstanceCreatedEvent) Reset() { @@ -1020,14 +1020,14 @@ func (x *SubOrchestrationInstanceCreatedEvent) GetName() string { return "" } -func (x *SubOrchestrationInstanceCreatedEvent) GetVersion() *wrappers.StringValue { +func (x *SubOrchestrationInstanceCreatedEvent) GetVersion() *wrapperspb.StringValue { if x != nil { return x.Version } return nil } -func (x *SubOrchestrationInstanceCreatedEvent) GetInput() *wrappers.StringValue { +func (x *SubOrchestrationInstanceCreatedEvent) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } @@ -1046,8 +1046,8 @@ type SubOrchestrationInstanceCompletedEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - TaskScheduledId int32 `protobuf:"varint,1,opt,name=taskScheduledId,proto3" json:"taskScheduledId,omitempty"` - Result *wrappers.StringValue `protobuf:"bytes,2,opt,name=result,proto3" json:"result,omitempty"` + TaskScheduledId int32 `protobuf:"varint,1,opt,name=taskScheduledId,proto3" json:"taskScheduledId,omitempty"` + Result *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=result,proto3" json:"result,omitempty"` } func (x *SubOrchestrationInstanceCompletedEvent) Reset() { @@ -1089,7 +1089,7 @@ func (x *SubOrchestrationInstanceCompletedEvent) GetTaskScheduledId() int32 { return 0 } -func (x *SubOrchestrationInstanceCompletedEvent) GetResult() *wrappers.StringValue { +func (x *SubOrchestrationInstanceCompletedEvent) GetResult() *wrapperspb.StringValue { if x != nil { return x.Result } @@ -1156,7 +1156,7 @@ type TimerCreatedEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - FireAt *timestamp.Timestamp `protobuf:"bytes,1,opt,name=fireAt,proto3" json:"fireAt,omitempty"` + FireAt *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=fireAt,proto3" json:"fireAt,omitempty"` } func (x *TimerCreatedEvent) Reset() { @@ -1191,7 +1191,7 @@ func (*TimerCreatedEvent) Descriptor() ([]byte, []int) { return file_orchestrator_service_proto_rawDescGZIP(), []int{15} } -func (x *TimerCreatedEvent) GetFireAt() *timestamp.Timestamp { +func (x *TimerCreatedEvent) GetFireAt() *timestamppb.Timestamp { if x != nil { return x.FireAt } @@ -1203,8 +1203,8 @@ type TimerFiredEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - FireAt *timestamp.Timestamp `protobuf:"bytes,1,opt,name=fireAt,proto3" json:"fireAt,omitempty"` - TimerId int32 `protobuf:"varint,2,opt,name=timerId,proto3" json:"timerId,omitempty"` + FireAt *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=fireAt,proto3" json:"fireAt,omitempty"` + TimerId int32 `protobuf:"varint,2,opt,name=timerId,proto3" json:"timerId,omitempty"` } func (x *TimerFiredEvent) Reset() { @@ -1239,7 +1239,7 @@ func (*TimerFiredEvent) Descriptor() ([]byte, []int) { return file_orchestrator_service_proto_rawDescGZIP(), []int{16} } -func (x *TimerFiredEvent) GetFireAt() *timestamp.Timestamp { +func (x *TimerFiredEvent) GetFireAt() *timestamppb.Timestamp { if x != nil { return x.FireAt } @@ -1334,9 +1334,9 @@ type EventSentEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Input *wrappers.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` } func (x *EventSentEvent) Reset() { @@ -1385,7 +1385,7 @@ func (x *EventSentEvent) GetName() string { return "" } -func (x *EventSentEvent) GetInput() *wrappers.StringValue { +func (x *EventSentEvent) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } @@ -1397,8 +1397,8 @@ type EventRaisedEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Input *wrappers.StringValue `protobuf:"bytes,2,opt,name=input,proto3" json:"input,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=input,proto3" json:"input,omitempty"` } func (x *EventRaisedEvent) Reset() { @@ -1440,7 +1440,7 @@ func (x *EventRaisedEvent) GetName() string { return "" } -func (x *EventRaisedEvent) GetInput() *wrappers.StringValue { +func (x *EventRaisedEvent) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } @@ -1452,7 +1452,7 @@ type GenericEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Data *wrappers.StringValue `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + Data *wrapperspb.StringValue `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` } func (x *GenericEvent) Reset() { @@ -1487,7 +1487,7 @@ func (*GenericEvent) Descriptor() ([]byte, []int) { return file_orchestrator_service_proto_rawDescGZIP(), []int{21} } -func (x *GenericEvent) GetData() *wrappers.StringValue { +func (x *GenericEvent) GetData() *wrapperspb.StringValue { if x != nil { return x.Data } @@ -1546,7 +1546,7 @@ type ContinueAsNewEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Input *wrappers.StringValue `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` } func (x *ContinueAsNewEvent) Reset() { @@ -1581,7 +1581,7 @@ func (*ContinueAsNewEvent) Descriptor() ([]byte, []int) { return file_orchestrator_service_proto_rawDescGZIP(), []int{23} } -func (x *ContinueAsNewEvent) GetInput() *wrappers.StringValue { +func (x *ContinueAsNewEvent) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } @@ -1593,7 +1593,7 @@ type ExecutionSuspendedEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Input *wrappers.StringValue `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` } func (x *ExecutionSuspendedEvent) Reset() { @@ -1628,7 +1628,7 @@ func (*ExecutionSuspendedEvent) Descriptor() ([]byte, []int) { return file_orchestrator_service_proto_rawDescGZIP(), []int{24} } -func (x *ExecutionSuspendedEvent) GetInput() *wrappers.StringValue { +func (x *ExecutionSuspendedEvent) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } @@ -1640,7 +1640,7 @@ type ExecutionResumedEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Input *wrappers.StringValue `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` } func (x *ExecutionResumedEvent) Reset() { @@ -1675,7 +1675,7 @@ func (*ExecutionResumedEvent) Descriptor() ([]byte, []int) { return file_orchestrator_service_proto_rawDescGZIP(), []int{25} } -func (x *ExecutionResumedEvent) GetInput() *wrappers.StringValue { +func (x *ExecutionResumedEvent) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } @@ -1687,8 +1687,8 @@ type HistoryEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - EventId int32 `protobuf:"varint,1,opt,name=eventId,proto3" json:"eventId,omitempty"` - Timestamp *timestamp.Timestamp `protobuf:"bytes,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + EventId int32 `protobuf:"varint,1,opt,name=eventId,proto3" json:"eventId,omitempty"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // Types that are assignable to EventType: // // *HistoryEvent_ExecutionStarted @@ -1753,7 +1753,7 @@ func (x *HistoryEvent) GetEventId() int32 { return 0 } -func (x *HistoryEvent) GetTimestamp() *timestamp.Timestamp { +func (x *HistoryEvent) GetTimestamp() *timestamppb.Timestamp { if x != nil { return x.Timestamp } @@ -2036,9 +2036,9 @@ type ScheduleTaskAction struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Version *wrappers.StringValue `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` - Input *wrappers.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Version *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` } func (x *ScheduleTaskAction) Reset() { @@ -2080,14 +2080,14 @@ func (x *ScheduleTaskAction) GetName() string { return "" } -func (x *ScheduleTaskAction) GetVersion() *wrappers.StringValue { +func (x *ScheduleTaskAction) GetVersion() *wrapperspb.StringValue { if x != nil { return x.Version } return nil } -func (x *ScheduleTaskAction) GetInput() *wrappers.StringValue { +func (x *ScheduleTaskAction) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } @@ -2099,10 +2099,10 @@ type CreateSubOrchestrationAction struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Version *wrappers.StringValue `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` - Input *wrappers.StringValue `protobuf:"bytes,4,opt,name=input,proto3" json:"input,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Version *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,4,opt,name=input,proto3" json:"input,omitempty"` } func (x *CreateSubOrchestrationAction) Reset() { @@ -2151,14 +2151,14 @@ func (x *CreateSubOrchestrationAction) GetName() string { return "" } -func (x *CreateSubOrchestrationAction) GetVersion() *wrappers.StringValue { +func (x *CreateSubOrchestrationAction) GetVersion() *wrapperspb.StringValue { if x != nil { return x.Version } return nil } -func (x *CreateSubOrchestrationAction) GetInput() *wrappers.StringValue { +func (x *CreateSubOrchestrationAction) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } @@ -2170,7 +2170,7 @@ type CreateTimerAction struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - FireAt *timestamp.Timestamp `protobuf:"bytes,1,opt,name=fireAt,proto3" json:"fireAt,omitempty"` + FireAt *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=fireAt,proto3" json:"fireAt,omitempty"` } func (x *CreateTimerAction) Reset() { @@ -2205,7 +2205,7 @@ func (*CreateTimerAction) Descriptor() ([]byte, []int) { return file_orchestrator_service_proto_rawDescGZIP(), []int{29} } -func (x *CreateTimerAction) GetFireAt() *timestamp.Timestamp { +func (x *CreateTimerAction) GetFireAt() *timestamppb.Timestamp { if x != nil { return x.FireAt } @@ -2217,9 +2217,9 @@ type SendEventAction struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Instance *OrchestrationInstance `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Data *wrappers.StringValue `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` + Instance *OrchestrationInstance `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Data *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` } func (x *SendEventAction) Reset() { @@ -2268,7 +2268,7 @@ func (x *SendEventAction) GetName() string { return "" } -func (x *SendEventAction) GetData() *wrappers.StringValue { +func (x *SendEventAction) GetData() *wrapperspb.StringValue { if x != nil { return x.Data } @@ -2280,12 +2280,12 @@ type CompleteOrchestrationAction struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - OrchestrationStatus OrchestrationStatus `protobuf:"varint,1,opt,name=orchestrationStatus,proto3,enum=OrchestrationStatus" json:"orchestrationStatus,omitempty"` - Result *wrappers.StringValue `protobuf:"bytes,2,opt,name=result,proto3" json:"result,omitempty"` - Details *wrappers.StringValue `protobuf:"bytes,3,opt,name=details,proto3" json:"details,omitempty"` - NewVersion *wrappers.StringValue `protobuf:"bytes,4,opt,name=newVersion,proto3" json:"newVersion,omitempty"` - CarryoverEvents []*HistoryEvent `protobuf:"bytes,5,rep,name=carryoverEvents,proto3" json:"carryoverEvents,omitempty"` - FailureDetails *TaskFailureDetails `protobuf:"bytes,6,opt,name=failureDetails,proto3" json:"failureDetails,omitempty"` + OrchestrationStatus OrchestrationStatus `protobuf:"varint,1,opt,name=orchestrationStatus,proto3,enum=OrchestrationStatus" json:"orchestrationStatus,omitempty"` + Result *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=result,proto3" json:"result,omitempty"` + Details *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=details,proto3" json:"details,omitempty"` + NewVersion *wrapperspb.StringValue `protobuf:"bytes,4,opt,name=newVersion,proto3" json:"newVersion,omitempty"` + CarryoverEvents []*HistoryEvent `protobuf:"bytes,5,rep,name=carryoverEvents,proto3" json:"carryoverEvents,omitempty"` + FailureDetails *TaskFailureDetails `protobuf:"bytes,6,opt,name=failureDetails,proto3" json:"failureDetails,omitempty"` } func (x *CompleteOrchestrationAction) Reset() { @@ -2327,21 +2327,21 @@ func (x *CompleteOrchestrationAction) GetOrchestrationStatus() OrchestrationStat return OrchestrationStatus_ORCHESTRATION_STATUS_RUNNING } -func (x *CompleteOrchestrationAction) GetResult() *wrappers.StringValue { +func (x *CompleteOrchestrationAction) GetResult() *wrapperspb.StringValue { if x != nil { return x.Result } return nil } -func (x *CompleteOrchestrationAction) GetDetails() *wrappers.StringValue { +func (x *CompleteOrchestrationAction) GetDetails() *wrapperspb.StringValue { if x != nil { return x.Details } return nil } -func (x *CompleteOrchestrationAction) GetNewVersion() *wrappers.StringValue { +func (x *CompleteOrchestrationAction) GetNewVersion() *wrapperspb.StringValue { if x != nil { return x.NewVersion } @@ -2367,9 +2367,9 @@ type TerminateOrchestrationAction struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - Reason *wrappers.StringValue `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` - Recurse bool `protobuf:"varint,3,opt,name=recurse,proto3" json:"recurse,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + Reason *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + Recurse bool `protobuf:"varint,3,opt,name=recurse,proto3" json:"recurse,omitempty"` } func (x *TerminateOrchestrationAction) Reset() { @@ -2411,7 +2411,7 @@ func (x *TerminateOrchestrationAction) GetInstanceId() string { return "" } -func (x *TerminateOrchestrationAction) GetReason() *wrappers.StringValue { +func (x *TerminateOrchestrationAction) GetReason() *wrapperspb.StringValue { if x != nil { return x.Reason } @@ -2576,7 +2576,7 @@ type OrchestratorRequest struct { unknownFields protoimpl.UnknownFields InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - ExecutionId *wrappers.StringValue `protobuf:"bytes,2,opt,name=executionId,proto3" json:"executionId,omitempty"` + ExecutionId *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=executionId,proto3" json:"executionId,omitempty"` PastEvents []*HistoryEvent `protobuf:"bytes,3,rep,name=pastEvents,proto3" json:"pastEvents,omitempty"` NewEvents []*HistoryEvent `protobuf:"bytes,4,rep,name=newEvents,proto3" json:"newEvents,omitempty"` EntityParameters *OrchestratorEntityParameters `protobuf:"bytes,5,opt,name=entityParameters,proto3" json:"entityParameters,omitempty"` @@ -2621,7 +2621,7 @@ func (x *OrchestratorRequest) GetInstanceId() string { return "" } -func (x *OrchestratorRequest) GetExecutionId() *wrappers.StringValue { +func (x *OrchestratorRequest) GetExecutionId() *wrapperspb.StringValue { if x != nil { return x.ExecutionId } @@ -2654,9 +2654,9 @@ type OrchestratorResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - Actions []*OrchestratorAction `protobuf:"bytes,2,rep,name=actions,proto3" json:"actions,omitempty"` - CustomStatus *wrappers.StringValue `protobuf:"bytes,3,opt,name=customStatus,proto3" json:"customStatus,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + Actions []*OrchestratorAction `protobuf:"bytes,2,rep,name=actions,proto3" json:"actions,omitempty"` + CustomStatus *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=customStatus,proto3" json:"customStatus,omitempty"` } func (x *OrchestratorResponse) Reset() { @@ -2705,7 +2705,7 @@ func (x *OrchestratorResponse) GetActions() []*OrchestratorAction { return nil } -func (x *OrchestratorResponse) GetCustomStatus() *wrappers.StringValue { +func (x *OrchestratorResponse) GetCustomStatus() *wrapperspb.StringValue { if x != nil { return x.CustomStatus } @@ -2719,9 +2719,9 @@ type CreateInstanceRequest struct { InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Version *wrappers.StringValue `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` - Input *wrappers.StringValue `protobuf:"bytes,4,opt,name=input,proto3" json:"input,omitempty"` - ScheduledStartTimestamp *timestamp.Timestamp `protobuf:"bytes,5,opt,name=scheduledStartTimestamp,proto3" json:"scheduledStartTimestamp,omitempty"` + Version *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,4,opt,name=input,proto3" json:"input,omitempty"` + ScheduledStartTimestamp *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=scheduledStartTimestamp,proto3" json:"scheduledStartTimestamp,omitempty"` OrchestrationIdReusePolicy *OrchestrationIdReusePolicy `protobuf:"bytes,6,opt,name=orchestrationIdReusePolicy,proto3" json:"orchestrationIdReusePolicy,omitempty"` } @@ -2771,21 +2771,21 @@ func (x *CreateInstanceRequest) GetName() string { return "" } -func (x *CreateInstanceRequest) GetVersion() *wrappers.StringValue { +func (x *CreateInstanceRequest) GetVersion() *wrapperspb.StringValue { if x != nil { return x.Version } return nil } -func (x *CreateInstanceRequest) GetInput() *wrappers.StringValue { +func (x *CreateInstanceRequest) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } return nil } -func (x *CreateInstanceRequest) GetScheduledStartTimestamp() *timestamp.Timestamp { +func (x *CreateInstanceRequest) GetScheduledStartTimestamp() *timestamppb.Timestamp { if x != nil { return x.ScheduledStartTimestamp } @@ -3016,8 +3016,8 @@ type RewindInstanceRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - Reason *wrappers.StringValue `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + Reason *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` } func (x *RewindInstanceRequest) Reset() { @@ -3059,7 +3059,7 @@ func (x *RewindInstanceRequest) GetInstanceId() string { return "" } -func (x *RewindInstanceRequest) GetReason() *wrappers.StringValue { +func (x *RewindInstanceRequest) GetReason() *wrapperspb.StringValue { if x != nil { return x.Reason } @@ -3109,17 +3109,17 @@ type OrchestrationState struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Version *wrappers.StringValue `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` - OrchestrationStatus OrchestrationStatus `protobuf:"varint,4,opt,name=orchestrationStatus,proto3,enum=OrchestrationStatus" json:"orchestrationStatus,omitempty"` - ScheduledStartTimestamp *timestamp.Timestamp `protobuf:"bytes,5,opt,name=scheduledStartTimestamp,proto3" json:"scheduledStartTimestamp,omitempty"` - CreatedTimestamp *timestamp.Timestamp `protobuf:"bytes,6,opt,name=createdTimestamp,proto3" json:"createdTimestamp,omitempty"` - LastUpdatedTimestamp *timestamp.Timestamp `protobuf:"bytes,7,opt,name=lastUpdatedTimestamp,proto3" json:"lastUpdatedTimestamp,omitempty"` - Input *wrappers.StringValue `protobuf:"bytes,8,opt,name=input,proto3" json:"input,omitempty"` - Output *wrappers.StringValue `protobuf:"bytes,9,opt,name=output,proto3" json:"output,omitempty"` - CustomStatus *wrappers.StringValue `protobuf:"bytes,10,opt,name=customStatus,proto3" json:"customStatus,omitempty"` - FailureDetails *TaskFailureDetails `protobuf:"bytes,11,opt,name=failureDetails,proto3" json:"failureDetails,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Version *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + OrchestrationStatus OrchestrationStatus `protobuf:"varint,4,opt,name=orchestrationStatus,proto3,enum=OrchestrationStatus" json:"orchestrationStatus,omitempty"` + ScheduledStartTimestamp *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=scheduledStartTimestamp,proto3" json:"scheduledStartTimestamp,omitempty"` + CreatedTimestamp *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=createdTimestamp,proto3" json:"createdTimestamp,omitempty"` + LastUpdatedTimestamp *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=lastUpdatedTimestamp,proto3" json:"lastUpdatedTimestamp,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,8,opt,name=input,proto3" json:"input,omitempty"` + Output *wrapperspb.StringValue `protobuf:"bytes,9,opt,name=output,proto3" json:"output,omitempty"` + CustomStatus *wrapperspb.StringValue `protobuf:"bytes,10,opt,name=customStatus,proto3" json:"customStatus,omitempty"` + FailureDetails *TaskFailureDetails `protobuf:"bytes,11,opt,name=failureDetails,proto3" json:"failureDetails,omitempty"` } func (x *OrchestrationState) Reset() { @@ -3168,7 +3168,7 @@ func (x *OrchestrationState) GetName() string { return "" } -func (x *OrchestrationState) GetVersion() *wrappers.StringValue { +func (x *OrchestrationState) GetVersion() *wrapperspb.StringValue { if x != nil { return x.Version } @@ -3182,42 +3182,42 @@ func (x *OrchestrationState) GetOrchestrationStatus() OrchestrationStatus { return OrchestrationStatus_ORCHESTRATION_STATUS_RUNNING } -func (x *OrchestrationState) GetScheduledStartTimestamp() *timestamp.Timestamp { +func (x *OrchestrationState) GetScheduledStartTimestamp() *timestamppb.Timestamp { if x != nil { return x.ScheduledStartTimestamp } return nil } -func (x *OrchestrationState) GetCreatedTimestamp() *timestamp.Timestamp { +func (x *OrchestrationState) GetCreatedTimestamp() *timestamppb.Timestamp { if x != nil { return x.CreatedTimestamp } return nil } -func (x *OrchestrationState) GetLastUpdatedTimestamp() *timestamp.Timestamp { +func (x *OrchestrationState) GetLastUpdatedTimestamp() *timestamppb.Timestamp { if x != nil { return x.LastUpdatedTimestamp } return nil } -func (x *OrchestrationState) GetInput() *wrappers.StringValue { +func (x *OrchestrationState) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } return nil } -func (x *OrchestrationState) GetOutput() *wrappers.StringValue { +func (x *OrchestrationState) GetOutput() *wrapperspb.StringValue { if x != nil { return x.Output } return nil } -func (x *OrchestrationState) GetCustomStatus() *wrappers.StringValue { +func (x *OrchestrationState) GetCustomStatus() *wrapperspb.StringValue { if x != nil { return x.CustomStatus } @@ -3236,9 +3236,9 @@ type RaiseEventRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Input *wrappers.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` } func (x *RaiseEventRequest) Reset() { @@ -3287,7 +3287,7 @@ func (x *RaiseEventRequest) GetName() string { return "" } -func (x *RaiseEventRequest) GetInput() *wrappers.StringValue { +func (x *RaiseEventRequest) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } @@ -3337,9 +3337,9 @@ type TerminateRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - Output *wrappers.StringValue `protobuf:"bytes,2,opt,name=output,proto3" json:"output,omitempty"` - Recursive bool `protobuf:"varint,3,opt,name=recursive,proto3" json:"recursive,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + Output *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=output,proto3" json:"output,omitempty"` + Recursive bool `protobuf:"varint,3,opt,name=recursive,proto3" json:"recursive,omitempty"` } func (x *TerminateRequest) Reset() { @@ -3381,7 +3381,7 @@ func (x *TerminateRequest) GetInstanceId() string { return "" } -func (x *TerminateRequest) GetOutput() *wrappers.StringValue { +func (x *TerminateRequest) GetOutput() *wrapperspb.StringValue { if x != nil { return x.Output } @@ -3438,8 +3438,8 @@ type SuspendRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - Reason *wrappers.StringValue `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + Reason *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` } func (x *SuspendRequest) Reset() { @@ -3481,7 +3481,7 @@ func (x *SuspendRequest) GetInstanceId() string { return "" } -func (x *SuspendRequest) GetReason() *wrappers.StringValue { +func (x *SuspendRequest) GetReason() *wrapperspb.StringValue { if x != nil { return x.Reason } @@ -3531,8 +3531,8 @@ type ResumeRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - Reason *wrappers.StringValue `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + Reason *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` } func (x *ResumeRequest) Reset() { @@ -3574,7 +3574,7 @@ func (x *ResumeRequest) GetInstanceId() string { return "" } -func (x *ResumeRequest) GetReason() *wrappers.StringValue { +func (x *ResumeRequest) GetReason() *wrapperspb.StringValue { if x != nil { return x.Reason } @@ -3671,14 +3671,14 @@ type InstanceQuery struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - RuntimeStatus []OrchestrationStatus `protobuf:"varint,1,rep,packed,name=runtimeStatus,proto3,enum=OrchestrationStatus" json:"runtimeStatus,omitempty"` - CreatedTimeFrom *timestamp.Timestamp `protobuf:"bytes,2,opt,name=createdTimeFrom,proto3" json:"createdTimeFrom,omitempty"` - CreatedTimeTo *timestamp.Timestamp `protobuf:"bytes,3,opt,name=createdTimeTo,proto3" json:"createdTimeTo,omitempty"` - TaskHubNames []*wrappers.StringValue `protobuf:"bytes,4,rep,name=taskHubNames,proto3" json:"taskHubNames,omitempty"` - MaxInstanceCount int32 `protobuf:"varint,5,opt,name=maxInstanceCount,proto3" json:"maxInstanceCount,omitempty"` - ContinuationToken *wrappers.StringValue `protobuf:"bytes,6,opt,name=continuationToken,proto3" json:"continuationToken,omitempty"` - InstanceIdPrefix *wrappers.StringValue `protobuf:"bytes,7,opt,name=instanceIdPrefix,proto3" json:"instanceIdPrefix,omitempty"` - FetchInputsAndOutputs bool `protobuf:"varint,8,opt,name=fetchInputsAndOutputs,proto3" json:"fetchInputsAndOutputs,omitempty"` + RuntimeStatus []OrchestrationStatus `protobuf:"varint,1,rep,packed,name=runtimeStatus,proto3,enum=OrchestrationStatus" json:"runtimeStatus,omitempty"` + CreatedTimeFrom *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=createdTimeFrom,proto3" json:"createdTimeFrom,omitempty"` + CreatedTimeTo *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=createdTimeTo,proto3" json:"createdTimeTo,omitempty"` + TaskHubNames []*wrapperspb.StringValue `protobuf:"bytes,4,rep,name=taskHubNames,proto3" json:"taskHubNames,omitempty"` + MaxInstanceCount int32 `protobuf:"varint,5,opt,name=maxInstanceCount,proto3" json:"maxInstanceCount,omitempty"` + ContinuationToken *wrapperspb.StringValue `protobuf:"bytes,6,opt,name=continuationToken,proto3" json:"continuationToken,omitempty"` + InstanceIdPrefix *wrapperspb.StringValue `protobuf:"bytes,7,opt,name=instanceIdPrefix,proto3" json:"instanceIdPrefix,omitempty"` + FetchInputsAndOutputs bool `protobuf:"varint,8,opt,name=fetchInputsAndOutputs,proto3" json:"fetchInputsAndOutputs,omitempty"` } func (x *InstanceQuery) Reset() { @@ -3720,21 +3720,21 @@ func (x *InstanceQuery) GetRuntimeStatus() []OrchestrationStatus { return nil } -func (x *InstanceQuery) GetCreatedTimeFrom() *timestamp.Timestamp { +func (x *InstanceQuery) GetCreatedTimeFrom() *timestamppb.Timestamp { if x != nil { return x.CreatedTimeFrom } return nil } -func (x *InstanceQuery) GetCreatedTimeTo() *timestamp.Timestamp { +func (x *InstanceQuery) GetCreatedTimeTo() *timestamppb.Timestamp { if x != nil { return x.CreatedTimeTo } return nil } -func (x *InstanceQuery) GetTaskHubNames() []*wrappers.StringValue { +func (x *InstanceQuery) GetTaskHubNames() []*wrapperspb.StringValue { if x != nil { return x.TaskHubNames } @@ -3748,14 +3748,14 @@ func (x *InstanceQuery) GetMaxInstanceCount() int32 { return 0 } -func (x *InstanceQuery) GetContinuationToken() *wrappers.StringValue { +func (x *InstanceQuery) GetContinuationToken() *wrapperspb.StringValue { if x != nil { return x.ContinuationToken } return nil } -func (x *InstanceQuery) GetInstanceIdPrefix() *wrappers.StringValue { +func (x *InstanceQuery) GetInstanceIdPrefix() *wrapperspb.StringValue { if x != nil { return x.InstanceIdPrefix } @@ -3774,8 +3774,8 @@ type QueryInstancesResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - OrchestrationState []*OrchestrationState `protobuf:"bytes,1,rep,name=orchestrationState,proto3" json:"orchestrationState,omitempty"` - ContinuationToken *wrappers.StringValue `protobuf:"bytes,2,opt,name=continuationToken,proto3" json:"continuationToken,omitempty"` + OrchestrationState []*OrchestrationState `protobuf:"bytes,1,rep,name=orchestrationState,proto3" json:"orchestrationState,omitempty"` + ContinuationToken *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=continuationToken,proto3" json:"continuationToken,omitempty"` } func (x *QueryInstancesResponse) Reset() { @@ -3817,7 +3817,7 @@ func (x *QueryInstancesResponse) GetOrchestrationState() []*OrchestrationState { return nil } -func (x *QueryInstancesResponse) GetContinuationToken() *wrappers.StringValue { +func (x *QueryInstancesResponse) GetContinuationToken() *wrapperspb.StringValue { if x != nil { return x.ContinuationToken } @@ -3918,9 +3918,9 @@ type PurgeInstanceFilter struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - CreatedTimeFrom *timestamp.Timestamp `protobuf:"bytes,1,opt,name=createdTimeFrom,proto3" json:"createdTimeFrom,omitempty"` - CreatedTimeTo *timestamp.Timestamp `protobuf:"bytes,2,opt,name=createdTimeTo,proto3" json:"createdTimeTo,omitempty"` - RuntimeStatus []OrchestrationStatus `protobuf:"varint,3,rep,packed,name=runtimeStatus,proto3,enum=OrchestrationStatus" json:"runtimeStatus,omitempty"` + CreatedTimeFrom *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=createdTimeFrom,proto3" json:"createdTimeFrom,omitempty"` + CreatedTimeTo *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=createdTimeTo,proto3" json:"createdTimeTo,omitempty"` + RuntimeStatus []OrchestrationStatus `protobuf:"varint,3,rep,packed,name=runtimeStatus,proto3,enum=OrchestrationStatus" json:"runtimeStatus,omitempty"` } func (x *PurgeInstanceFilter) Reset() { @@ -3955,14 +3955,14 @@ func (*PurgeInstanceFilter) Descriptor() ([]byte, []int) { return file_orchestrator_service_proto_rawDescGZIP(), []int{56} } -func (x *PurgeInstanceFilter) GetCreatedTimeFrom() *timestamp.Timestamp { +func (x *PurgeInstanceFilter) GetCreatedTimeFrom() *timestamppb.Timestamp { if x != nil { return x.CreatedTimeFrom } return nil } -func (x *PurgeInstanceFilter) GetCreatedTimeTo() *timestamp.Timestamp { +func (x *PurgeInstanceFilter) GetCreatedTimeTo() *timestamppb.Timestamp { if x != nil { return x.CreatedTimeTo } @@ -4189,11 +4189,11 @@ type SignalEntityRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Input *wrappers.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` - RequestId string `protobuf:"bytes,4,opt,name=requestId,proto3" json:"requestId,omitempty"` - ScheduledTime *timestamp.Timestamp `protobuf:"bytes,5,opt,name=scheduledTime,proto3" json:"scheduledTime,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` + RequestId string `protobuf:"bytes,4,opt,name=requestId,proto3" json:"requestId,omitempty"` + ScheduledTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=scheduledTime,proto3" json:"scheduledTime,omitempty"` } func (x *SignalEntityRequest) Reset() { @@ -4242,7 +4242,7 @@ func (x *SignalEntityRequest) GetName() string { return "" } -func (x *SignalEntityRequest) GetInput() *wrappers.StringValue { +func (x *SignalEntityRequest) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } @@ -4256,7 +4256,7 @@ func (x *SignalEntityRequest) GetRequestId() string { return "" } -func (x *SignalEntityRequest) GetScheduledTime() *timestamp.Timestamp { +func (x *SignalEntityRequest) GetScheduledTime() *timestamppb.Timestamp { if x != nil { return x.ScheduledTime } @@ -4416,13 +4416,13 @@ type EntityQuery struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceIdStartsWith *wrappers.StringValue `protobuf:"bytes,1,opt,name=instanceIdStartsWith,proto3" json:"instanceIdStartsWith,omitempty"` - LastModifiedFrom *timestamp.Timestamp `protobuf:"bytes,2,opt,name=lastModifiedFrom,proto3" json:"lastModifiedFrom,omitempty"` - LastModifiedTo *timestamp.Timestamp `protobuf:"bytes,3,opt,name=lastModifiedTo,proto3" json:"lastModifiedTo,omitempty"` - IncludeState bool `protobuf:"varint,4,opt,name=includeState,proto3" json:"includeState,omitempty"` - IncludeTransient bool `protobuf:"varint,5,opt,name=includeTransient,proto3" json:"includeTransient,omitempty"` - PageSize *wrappers.Int32Value `protobuf:"bytes,6,opt,name=pageSize,proto3" json:"pageSize,omitempty"` - ContinuationToken *wrappers.StringValue `protobuf:"bytes,7,opt,name=continuationToken,proto3" json:"continuationToken,omitempty"` + InstanceIdStartsWith *wrapperspb.StringValue `protobuf:"bytes,1,opt,name=instanceIdStartsWith,proto3" json:"instanceIdStartsWith,omitempty"` + LastModifiedFrom *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=lastModifiedFrom,proto3" json:"lastModifiedFrom,omitempty"` + LastModifiedTo *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=lastModifiedTo,proto3" json:"lastModifiedTo,omitempty"` + IncludeState bool `protobuf:"varint,4,opt,name=includeState,proto3" json:"includeState,omitempty"` + IncludeTransient bool `protobuf:"varint,5,opt,name=includeTransient,proto3" json:"includeTransient,omitempty"` + PageSize *wrapperspb.Int32Value `protobuf:"bytes,6,opt,name=pageSize,proto3" json:"pageSize,omitempty"` + ContinuationToken *wrapperspb.StringValue `protobuf:"bytes,7,opt,name=continuationToken,proto3" json:"continuationToken,omitempty"` } func (x *EntityQuery) Reset() { @@ -4457,21 +4457,21 @@ func (*EntityQuery) Descriptor() ([]byte, []int) { return file_orchestrator_service_proto_rawDescGZIP(), []int{66} } -func (x *EntityQuery) GetInstanceIdStartsWith() *wrappers.StringValue { +func (x *EntityQuery) GetInstanceIdStartsWith() *wrapperspb.StringValue { if x != nil { return x.InstanceIdStartsWith } return nil } -func (x *EntityQuery) GetLastModifiedFrom() *timestamp.Timestamp { +func (x *EntityQuery) GetLastModifiedFrom() *timestamppb.Timestamp { if x != nil { return x.LastModifiedFrom } return nil } -func (x *EntityQuery) GetLastModifiedTo() *timestamp.Timestamp { +func (x *EntityQuery) GetLastModifiedTo() *timestamppb.Timestamp { if x != nil { return x.LastModifiedTo } @@ -4492,14 +4492,14 @@ func (x *EntityQuery) GetIncludeTransient() bool { return false } -func (x *EntityQuery) GetPageSize() *wrappers.Int32Value { +func (x *EntityQuery) GetPageSize() *wrapperspb.Int32Value { if x != nil { return x.PageSize } return nil } -func (x *EntityQuery) GetContinuationToken() *wrappers.StringValue { +func (x *EntityQuery) GetContinuationToken() *wrapperspb.StringValue { if x != nil { return x.ContinuationToken } @@ -4558,8 +4558,8 @@ type QueryEntitiesResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Entities []*EntityMetadata `protobuf:"bytes,1,rep,name=entities,proto3" json:"entities,omitempty"` - ContinuationToken *wrappers.StringValue `protobuf:"bytes,2,opt,name=continuationToken,proto3" json:"continuationToken,omitempty"` + Entities []*EntityMetadata `protobuf:"bytes,1,rep,name=entities,proto3" json:"entities,omitempty"` + ContinuationToken *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=continuationToken,proto3" json:"continuationToken,omitempty"` } func (x *QueryEntitiesResponse) Reset() { @@ -4601,7 +4601,7 @@ func (x *QueryEntitiesResponse) GetEntities() []*EntityMetadata { return nil } -func (x *QueryEntitiesResponse) GetContinuationToken() *wrappers.StringValue { +func (x *QueryEntitiesResponse) GetContinuationToken() *wrapperspb.StringValue { if x != nil { return x.ContinuationToken } @@ -4613,11 +4613,11 @@ type EntityMetadata struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - LastModifiedTime *timestamp.Timestamp `protobuf:"bytes,2,opt,name=lastModifiedTime,proto3" json:"lastModifiedTime,omitempty"` - BacklogQueueSize int32 `protobuf:"varint,3,opt,name=backlogQueueSize,proto3" json:"backlogQueueSize,omitempty"` - LockedBy *wrappers.StringValue `protobuf:"bytes,4,opt,name=lockedBy,proto3" json:"lockedBy,omitempty"` - SerializedState *wrappers.StringValue `protobuf:"bytes,5,opt,name=serializedState,proto3" json:"serializedState,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + LastModifiedTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=lastModifiedTime,proto3" json:"lastModifiedTime,omitempty"` + BacklogQueueSize int32 `protobuf:"varint,3,opt,name=backlogQueueSize,proto3" json:"backlogQueueSize,omitempty"` + LockedBy *wrapperspb.StringValue `protobuf:"bytes,4,opt,name=lockedBy,proto3" json:"lockedBy,omitempty"` + SerializedState *wrapperspb.StringValue `protobuf:"bytes,5,opt,name=serializedState,proto3" json:"serializedState,omitempty"` } func (x *EntityMetadata) Reset() { @@ -4659,7 +4659,7 @@ func (x *EntityMetadata) GetInstanceId() string { return "" } -func (x *EntityMetadata) GetLastModifiedTime() *timestamp.Timestamp { +func (x *EntityMetadata) GetLastModifiedTime() *timestamppb.Timestamp { if x != nil { return x.LastModifiedTime } @@ -4673,14 +4673,14 @@ func (x *EntityMetadata) GetBacklogQueueSize() int32 { return 0 } -func (x *EntityMetadata) GetLockedBy() *wrappers.StringValue { +func (x *EntityMetadata) GetLockedBy() *wrapperspb.StringValue { if x != nil { return x.LockedBy } return nil } -func (x *EntityMetadata) GetSerializedState() *wrappers.StringValue { +func (x *EntityMetadata) GetSerializedState() *wrapperspb.StringValue { if x != nil { return x.SerializedState } @@ -4692,9 +4692,9 @@ type CleanEntityStorageRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ContinuationToken *wrappers.StringValue `protobuf:"bytes,1,opt,name=continuationToken,proto3" json:"continuationToken,omitempty"` - RemoveEmptyEntities bool `protobuf:"varint,2,opt,name=removeEmptyEntities,proto3" json:"removeEmptyEntities,omitempty"` - ReleaseOrphanedLocks bool `protobuf:"varint,3,opt,name=releaseOrphanedLocks,proto3" json:"releaseOrphanedLocks,omitempty"` + ContinuationToken *wrapperspb.StringValue `protobuf:"bytes,1,opt,name=continuationToken,proto3" json:"continuationToken,omitempty"` + RemoveEmptyEntities bool `protobuf:"varint,2,opt,name=removeEmptyEntities,proto3" json:"removeEmptyEntities,omitempty"` + ReleaseOrphanedLocks bool `protobuf:"varint,3,opt,name=releaseOrphanedLocks,proto3" json:"releaseOrphanedLocks,omitempty"` } func (x *CleanEntityStorageRequest) Reset() { @@ -4729,7 +4729,7 @@ func (*CleanEntityStorageRequest) Descriptor() ([]byte, []int) { return file_orchestrator_service_proto_rawDescGZIP(), []int{70} } -func (x *CleanEntityStorageRequest) GetContinuationToken() *wrappers.StringValue { +func (x *CleanEntityStorageRequest) GetContinuationToken() *wrapperspb.StringValue { if x != nil { return x.ContinuationToken } @@ -4755,9 +4755,9 @@ type CleanEntityStorageResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ContinuationToken *wrappers.StringValue `protobuf:"bytes,1,opt,name=continuationToken,proto3" json:"continuationToken,omitempty"` - EmptyEntitiesRemoved int32 `protobuf:"varint,2,opt,name=emptyEntitiesRemoved,proto3" json:"emptyEntitiesRemoved,omitempty"` - OrphanedLocksReleased int32 `protobuf:"varint,3,opt,name=orphanedLocksReleased,proto3" json:"orphanedLocksReleased,omitempty"` + ContinuationToken *wrapperspb.StringValue `protobuf:"bytes,1,opt,name=continuationToken,proto3" json:"continuationToken,omitempty"` + EmptyEntitiesRemoved int32 `protobuf:"varint,2,opt,name=emptyEntitiesRemoved,proto3" json:"emptyEntitiesRemoved,omitempty"` + OrphanedLocksReleased int32 `protobuf:"varint,3,opt,name=orphanedLocksReleased,proto3" json:"orphanedLocksReleased,omitempty"` } func (x *CleanEntityStorageResponse) Reset() { @@ -4792,7 +4792,7 @@ func (*CleanEntityStorageResponse) Descriptor() ([]byte, []int) { return file_orchestrator_service_proto_rawDescGZIP(), []int{71} } -func (x *CleanEntityStorageResponse) GetContinuationToken() *wrappers.StringValue { +func (x *CleanEntityStorageResponse) GetContinuationToken() *wrapperspb.StringValue { if x != nil { return x.ContinuationToken } @@ -4818,7 +4818,7 @@ type OrchestratorEntityParameters struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - EntityMessageReorderWindow *duration.Duration `protobuf:"bytes,1,opt,name=entityMessageReorderWindow,proto3" json:"entityMessageReorderWindow,omitempty"` + EntityMessageReorderWindow *durationpb.Duration `protobuf:"bytes,1,opt,name=entityMessageReorderWindow,proto3" json:"entityMessageReorderWindow,omitempty"` } func (x *OrchestratorEntityParameters) Reset() { @@ -4853,7 +4853,7 @@ func (*OrchestratorEntityParameters) Descriptor() ([]byte, []int) { return file_orchestrator_service_proto_rawDescGZIP(), []int{72} } -func (x *OrchestratorEntityParameters) GetEntityMessageReorderWindow() *duration.Duration { +func (x *OrchestratorEntityParameters) GetEntityMessageReorderWindow() *durationpb.Duration { if x != nil { return x.EntityMessageReorderWindow } @@ -4865,9 +4865,9 @@ type EntityBatchRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - EntityState *wrappers.StringValue `protobuf:"bytes,2,opt,name=entityState,proto3" json:"entityState,omitempty"` - Operations []*OperationRequest `protobuf:"bytes,3,rep,name=operations,proto3" json:"operations,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + EntityState *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=entityState,proto3" json:"entityState,omitempty"` + Operations []*OperationRequest `protobuf:"bytes,3,rep,name=operations,proto3" json:"operations,omitempty"` } func (x *EntityBatchRequest) Reset() { @@ -4909,7 +4909,7 @@ func (x *EntityBatchRequest) GetInstanceId() string { return "" } -func (x *EntityBatchRequest) GetEntityState() *wrappers.StringValue { +func (x *EntityBatchRequest) GetEntityState() *wrapperspb.StringValue { if x != nil { return x.EntityState } @@ -4928,10 +4928,10 @@ type EntityBatchResult struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Results []*OperationResult `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"` - Actions []*OperationAction `protobuf:"bytes,2,rep,name=actions,proto3" json:"actions,omitempty"` - EntityState *wrappers.StringValue `protobuf:"bytes,3,opt,name=entityState,proto3" json:"entityState,omitempty"` - FailureDetails *TaskFailureDetails `protobuf:"bytes,4,opt,name=failureDetails,proto3" json:"failureDetails,omitempty"` + Results []*OperationResult `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"` + Actions []*OperationAction `protobuf:"bytes,2,rep,name=actions,proto3" json:"actions,omitempty"` + EntityState *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=entityState,proto3" json:"entityState,omitempty"` + FailureDetails *TaskFailureDetails `protobuf:"bytes,4,opt,name=failureDetails,proto3" json:"failureDetails,omitempty"` } func (x *EntityBatchResult) Reset() { @@ -4980,7 +4980,7 @@ func (x *EntityBatchResult) GetActions() []*OperationAction { return nil } -func (x *EntityBatchResult) GetEntityState() *wrappers.StringValue { +func (x *EntityBatchResult) GetEntityState() *wrapperspb.StringValue { if x != nil { return x.EntityState } @@ -4999,9 +4999,9 @@ type OperationRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Operation string `protobuf:"bytes,1,opt,name=operation,proto3" json:"operation,omitempty"` - RequestId string `protobuf:"bytes,2,opt,name=requestId,proto3" json:"requestId,omitempty"` - Input *wrappers.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` + Operation string `protobuf:"bytes,1,opt,name=operation,proto3" json:"operation,omitempty"` + RequestId string `protobuf:"bytes,2,opt,name=requestId,proto3" json:"requestId,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` } func (x *OperationRequest) Reset() { @@ -5050,7 +5050,7 @@ func (x *OperationRequest) GetRequestId() string { return "" } -func (x *OperationRequest) GetInput() *wrappers.StringValue { +func (x *OperationRequest) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } @@ -5143,7 +5143,7 @@ type OperationResultSuccess struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Result *wrappers.StringValue `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` + Result *wrapperspb.StringValue `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` } func (x *OperationResultSuccess) Reset() { @@ -5178,7 +5178,7 @@ func (*OperationResultSuccess) Descriptor() ([]byte, []int) { return file_orchestrator_service_proto_rawDescGZIP(), []int{77} } -func (x *OperationResultSuccess) GetResult() *wrappers.StringValue { +func (x *OperationResultSuccess) GetResult() *wrapperspb.StringValue { if x != nil { return x.Result } @@ -5326,10 +5326,10 @@ type SendSignalAction struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Input *wrappers.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` - ScheduledTime *timestamp.Timestamp `protobuf:"bytes,4,opt,name=scheduledTime,proto3" json:"scheduledTime,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` + ScheduledTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=scheduledTime,proto3" json:"scheduledTime,omitempty"` } func (x *SendSignalAction) Reset() { @@ -5378,14 +5378,14 @@ func (x *SendSignalAction) GetName() string { return "" } -func (x *SendSignalAction) GetInput() *wrappers.StringValue { +func (x *SendSignalAction) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } return nil } -func (x *SendSignalAction) GetScheduledTime() *timestamp.Timestamp { +func (x *SendSignalAction) GetScheduledTime() *timestamppb.Timestamp { if x != nil { return x.ScheduledTime } @@ -5397,11 +5397,11 @@ type StartNewOrchestrationAction struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Version *wrappers.StringValue `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` - Input *wrappers.StringValue `protobuf:"bytes,4,opt,name=input,proto3" json:"input,omitempty"` - ScheduledTime *timestamp.Timestamp `protobuf:"bytes,5,opt,name=scheduledTime,proto3" json:"scheduledTime,omitempty"` + InstanceId string `protobuf:"bytes,1,opt,name=instanceId,proto3" json:"instanceId,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Version *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + Input *wrapperspb.StringValue `protobuf:"bytes,4,opt,name=input,proto3" json:"input,omitempty"` + ScheduledTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=scheduledTime,proto3" json:"scheduledTime,omitempty"` } func (x *StartNewOrchestrationAction) Reset() { @@ -5450,21 +5450,21 @@ func (x *StartNewOrchestrationAction) GetName() string { return "" } -func (x *StartNewOrchestrationAction) GetVersion() *wrappers.StringValue { +func (x *StartNewOrchestrationAction) GetVersion() *wrapperspb.StringValue { if x != nil { return x.Version } return nil } -func (x *StartNewOrchestrationAction) GetInput() *wrappers.StringValue { +func (x *StartNewOrchestrationAction) GetInput() *wrapperspb.StringValue { if x != nil { return x.Input } return nil } -func (x *StartNewOrchestrationAction) GetScheduledTime() *timestamp.Timestamp { +func (x *StartNewOrchestrationAction) GetScheduledTime() *timestamppb.Timestamp { if x != nil { return x.ScheduledTime } @@ -6796,11 +6796,11 @@ var file_orchestrator_service_proto_goTypes = []interface{}{ (*GetWorkItemsRequest)(nil), // 84: GetWorkItemsRequest (*WorkItem)(nil), // 85: WorkItem (*CompleteTaskResponse)(nil), // 86: CompleteTaskResponse - (*wrappers.StringValue)(nil), // 87: google.protobuf.StringValue - (*timestamp.Timestamp)(nil), // 88: google.protobuf.Timestamp - (*wrappers.Int32Value)(nil), // 89: google.protobuf.Int32Value - (*duration.Duration)(nil), // 90: google.protobuf.Duration - (*empty.Empty)(nil), // 91: google.protobuf.Empty + (*wrapperspb.StringValue)(nil), // 87: google.protobuf.StringValue + (*timestamppb.Timestamp)(nil), // 88: google.protobuf.Timestamp + (*wrapperspb.Int32Value)(nil), // 89: google.protobuf.Int32Value + (*durationpb.Duration)(nil), // 90: google.protobuf.Duration + (*emptypb.Empty)(nil), // 91: google.protobuf.Empty } var file_orchestrator_service_proto_depIdxs = []int32{ 87, // 0: OrchestrationInstance.executionId:type_name -> google.protobuf.StringValue diff --git a/internal/protos/orchestrator_service_grpc.pb.go b/internal/protos/orchestrator_service_grpc.pb.go index 0bc36435..7524f982 100644 --- a/internal/protos/orchestrator_service_grpc.pb.go +++ b/internal/protos/orchestrator_service_grpc.pb.go @@ -4,17 +4,17 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v3.12.4 +// - protoc v7.34.0 // source: orchestrator_service.proto package protos import ( context "context" - empty "google.golang.org/protobuf/types/known/emptypb" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" ) // This is a compile-time assertion to ensure that this generated file @@ -52,7 +52,7 @@ const ( // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type TaskHubSidecarServiceClient interface { // Sends a hello request to the sidecar service. - Hello(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*empty.Empty, error) + Hello(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) // Starts a new orchestration instance. StartInstance(ctx context.Context, in *CreateInstanceRequest, opts ...grpc.CallOption) (*CreateInstanceResponse, error) // Gets the status of an existing orchestration instance. @@ -99,8 +99,8 @@ func NewTaskHubSidecarServiceClient(cc grpc.ClientConnInterface) TaskHubSidecarS return &taskHubSidecarServiceClient{cc} } -func (c *taskHubSidecarServiceClient) Hello(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*empty.Empty, error) { - out := new(empty.Empty) +func (c *taskHubSidecarServiceClient) Hello(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) err := c.cc.Invoke(ctx, TaskHubSidecarService_Hello_FullMethodName, in, out, opts...) if err != nil { return nil, err @@ -325,7 +325,7 @@ func (c *taskHubSidecarServiceClient) CleanEntityStorage(ctx context.Context, in // for forward compatibility type TaskHubSidecarServiceServer interface { // Sends a hello request to the sidecar service. - Hello(context.Context, *empty.Empty) (*empty.Empty, error) + Hello(context.Context, *emptypb.Empty) (*emptypb.Empty, error) // Starts a new orchestration instance. StartInstance(context.Context, *CreateInstanceRequest) (*CreateInstanceResponse, error) // Gets the status of an existing orchestration instance. @@ -369,7 +369,7 @@ type TaskHubSidecarServiceServer interface { type UnimplementedTaskHubSidecarServiceServer struct { } -func (UnimplementedTaskHubSidecarServiceServer) Hello(context.Context, *empty.Empty) (*empty.Empty, error) { +func (UnimplementedTaskHubSidecarServiceServer) Hello(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method Hello not implemented") } func (UnimplementedTaskHubSidecarServiceServer) StartInstance(context.Context, *CreateInstanceRequest) (*CreateInstanceResponse, error) { @@ -449,7 +449,7 @@ func RegisterTaskHubSidecarServiceServer(s grpc.ServiceRegistrar, srv TaskHubSid } func _TaskHubSidecarService_Hello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(empty.Empty) + in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } @@ -461,7 +461,7 @@ func _TaskHubSidecarService_Hello_Handler(srv interface{}, ctx context.Context, FullMethod: TaskHubSidecarService_Hello_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(TaskHubSidecarServiceServer).Hello(ctx, req.(*empty.Empty)) + return srv.(TaskHubSidecarServiceServer).Hello(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } diff --git a/samples/entity/entity.go b/samples/entity/entity.go new file mode 100644 index 00000000..a06d9f50 --- /dev/null +++ b/samples/entity/entity.go @@ -0,0 +1,171 @@ +// This sample demonstrates how to use durable entities with the Durable Task Go SDK. +// It shows two patterns: +// 1. A raw entity function (Counter) with manual operation dispatch +// 2. An auto-dispatch entity (BankAccount) where operations map to methods on a struct +package main + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/microsoft/durabletask-go/api" + "github.com/microsoft/durabletask-go/backend" + "github.com/microsoft/durabletask-go/backend/sqlite" + "github.com/microsoft/durabletask-go/task" +) + +func main() { + r := task.NewTaskRegistry() + + // Pattern 1: Register a raw entity function with manual dispatch + if err := r.AddEntityN("counter", CounterEntity); err != nil { + log.Fatalf("Failed to register counter entity: %v", err) + } + + // Pattern 2: Register an auto-dispatch entity backed by a struct + if err := r.AddEntityN("bankaccount", task.NewEntityFor[BankAccount]()); err != nil { + log.Fatalf("Failed to register bank account entity: %v", err) + } + + ctx := context.Background() + client, worker, err := Init(ctx, r) + if err != nil { + log.Fatalf("Failed to initialize: %v", err) + } + defer func() { + if err := worker.Shutdown(ctx); err != nil { + log.Printf("Failed to shutdown: %v", err) + } + }() + + // --- Demo 1: Counter entity (raw function) --- + fmt.Println("=== Counter Entity Demo ===") + counterID := api.NewEntityID("counter", "myCounter") + + // Signal the entity to perform operations + if err := client.SignalEntity(ctx, counterID, "add", api.WithSignalInput(10)); err != nil { + log.Printf("Failed to signal entity: %v", err) //nolint:gocritic // sample code, keeping simple + return + } + if err := client.SignalEntity(ctx, counterID, "add", api.WithSignalInput(5)); err != nil { + log.Printf("Failed to signal entity: %v", err) + return + } + if err := client.SignalEntity(ctx, counterID, "add", api.WithSignalInput(-3)); err != nil { + log.Printf("Failed to signal entity: %v", err) + return + } + + // Wait for processing + time.Sleep(3 * time.Second) + + // Query the entity state + meta, err := client.FetchEntityMetadata(ctx, counterID, true) + if err != nil { + log.Printf("Failed to fetch entity: %v", err) + return + } + fmt.Printf("Counter state: %s\n", meta.SerializedState) // Expected: 12 + + // --- Demo 2: BankAccount entity (auto-dispatch) --- + fmt.Println("\n=== Bank Account Entity Demo ===") + accountID := api.NewEntityID("bankaccount", "checking-001") + + if err := client.SignalEntity(ctx, accountID, "Deposit", api.WithSignalInput(1000)); err != nil { + log.Printf("Failed to signal entity: %v", err) + return + } + if err := client.SignalEntity(ctx, accountID, "Deposit", api.WithSignalInput(500)); err != nil { + log.Printf("Failed to signal entity: %v", err) + return + } + if err := client.SignalEntity(ctx, accountID, "Withdraw", api.WithSignalInput(200)); err != nil { + log.Printf("Failed to signal entity: %v", err) + return + } + + time.Sleep(3 * time.Second) + + meta, err = client.FetchEntityMetadata(ctx, accountID, true) + if err != nil { + log.Printf("Failed to fetch entity: %v", err) + return + } + fmt.Printf("Bank account state: %s\n", meta.SerializedState) // Expected: {"balance":1300} + + fmt.Println("\nDone!") +} + +// Init creates and initializes an in-memory client and worker pair. +func Init(ctx context.Context, r *task.TaskRegistry) (backend.EntityTaskHubClient, backend.TaskHubWorker, error) { + logger := backend.DefaultLogger() + be := sqlite.NewSqliteBackend(sqlite.NewSqliteOptions(""), logger) + executor := task.NewTaskExecutor(r) + orchestrationWorker := backend.NewOrchestrationWorker(be, executor, logger) + activityWorker := backend.NewActivityTaskWorker(be, executor, logger) + taskHubWorker := backend.NewTaskHubWorker(be, orchestrationWorker, activityWorker, logger) + if err := taskHubWorker.Start(ctx); err != nil { + return nil, nil, err + } + taskHubClient := backend.NewTaskHubClient(be) + return taskHubClient.(backend.EntityTaskHubClient), taskHubWorker, nil +} + +// --- Pattern 1: Raw entity function --- + +// CounterEntity is a simple counter entity that supports "add", "get", and "reset" operations. +func CounterEntity(ctx *task.EntityContext) (any, error) { + var count int + if ctx.HasState() { + if err := ctx.GetState(&count); err != nil { + return nil, err + } + } + + switch ctx.Operation { + case "add": + var amount int + if err := ctx.GetInput(&amount); err != nil { + return nil, err + } + count += amount + case "get": + // just return current value + case "reset": + count = 0 + default: + return nil, fmt.Errorf("unknown operation: %s", ctx.Operation) + } + + if err := ctx.SetState(count); err != nil { + return nil, err + } + return count, nil +} + +// --- Pattern 2: Auto-dispatch entity --- + +// BankAccount is a struct-based entity. Public methods are automatically +// dispatched by operation name (case-insensitive). +type BankAccount struct { + Balance int `json:"balance"` +} + +func (a *BankAccount) Deposit(amount int) (any, error) { + a.Balance += amount + return a.Balance, nil +} + +func (a *BankAccount) Withdraw(amount int) (any, error) { + if amount > a.Balance { + return nil, fmt.Errorf("insufficient funds: balance=%d, withdrawal=%d", a.Balance, amount) + } + a.Balance -= amount + return a.Balance, nil +} + +func (a *BankAccount) Get() (any, error) { + return a.Balance, nil +} diff --git a/task/entity.go b/task/entity.go new file mode 100644 index 00000000..93d4c1de --- /dev/null +++ b/task/entity.go @@ -0,0 +1,217 @@ +package task + +import ( + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/google/uuid" + "github.com/microsoft/durabletask-go/api" + "github.com/microsoft/durabletask-go/internal/helpers" + "github.com/microsoft/durabletask-go/internal/protos" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +// Entity is the functional interface for entity implementations. +// An entity function receives an EntityContext and returns a result and error. +type Entity func(ctx *EntityContext) (any, error) + +// EntityContext provides the execution context for an entity operation. +type EntityContext struct { + ID api.EntityID + Operation string + + rawInput []byte + state entityState + stateDirty bool + actions []*protos.OperationAction + actionIDSeq int32 +} + +type entityState struct { + value []byte + hasValue bool +} + +// GetInput unmarshals the serialized entity operation input and saves the result into [v]. +func (ctx *EntityContext) GetInput(v any) error { + return unmarshalData(ctx.rawInput, v) +} + +// HasState returns true if the entity has state set. +func (ctx *EntityContext) HasState() bool { + return ctx.state.hasValue +} + +// GetState unmarshals the entity state and saves the result into [v]. +func (ctx *EntityContext) GetState(v any) error { + if !ctx.state.hasValue { + return fmt.Errorf("entity has no state") + } + return unmarshalData(ctx.state.value, v) +} + +// SetState sets the entity state. The state must be JSON-serializable. +// Passing nil deletes the entity state. +func (ctx *EntityContext) SetState(state any) error { + ctx.stateDirty = true + if state == nil { + ctx.state.value = nil + ctx.state.hasValue = false + return nil + } + bytes, err := json.Marshal(state) + if err != nil { + return fmt.Errorf("failed to marshal entity state: %w", err) + } + ctx.state.value = bytes + ctx.state.hasValue = true + return nil +} + +// SignalEntity sends a fire-and-forget signal to another entity. +func (ctx *EntityContext) SignalEntity(entityID api.EntityID, operationName string, input any) error { + if err := helpers.ValidateEntityName(entityID.Name); err != nil { + return err + } + + var rawInput *wrapperspb.StringValue + if input != nil { + bytes, err := json.Marshal(input) + if err != nil { + return fmt.Errorf("failed to marshal signal input: %w", err) + } + rawInput = wrapperspb.String(string(bytes)) + } + + action := &protos.OperationAction{ + Id: ctx.nextActionID(), + OperationActionType: &protos.OperationAction_SendSignal{ + SendSignal: &protos.SendSignalAction{ + InstanceId: entityID.String(), + Name: operationName, + Input: rawInput, + }, + }, + } + ctx.actions = append(ctx.actions, action) + return nil +} + +// StartNewOrchestration schedules a new orchestration from within an entity operation. +func (ctx *EntityContext) StartNewOrchestration(name string, opts ...entityStartOrchestrationOption) error { + options := &entityStartOrchestrationOptions{} + for _, configure := range opts { + if err := configure(options); err != nil { + return err + } + } + if options.instanceID == "" { + id := uuid.New() + options.instanceID = hex.EncodeToString(id[:]) + } else if err := helpers.ValidateOrchestrationInstanceID(options.instanceID); err != nil { + return err + } + + action := &protos.OperationAction{ + Id: ctx.nextActionID(), + OperationActionType: &protos.OperationAction_StartNewOrchestration{ + StartNewOrchestration: &protos.StartNewOrchestrationAction{ + InstanceId: options.instanceID, + Name: name, + Input: options.rawInput, + }, + }, + } + ctx.actions = append(ctx.actions, action) + return nil +} + +func (ctx *EntityContext) nextActionID() int32 { + id := ctx.actionIDSeq + ctx.actionIDSeq++ + return id +} + +// entityStartOrchestrationOptions holds options for starting orchestrations from entities. +type entityStartOrchestrationOptions struct { + instanceID string + rawInput *wrapperspb.StringValue +} + +// entityStartOrchestrationOption is a functional option for StartNewOrchestration. +type entityStartOrchestrationOption func(*entityStartOrchestrationOptions) error + +// WithEntityStartOrchestrationInput sets the input for the new orchestration. +func WithEntityStartOrchestrationInput(input any) entityStartOrchestrationOption { + return func(opts *entityStartOrchestrationOptions) error { + bytes, err := json.Marshal(input) + if err != nil { + return fmt.Errorf("failed to marshal orchestration input: %w", err) + } + opts.rawInput = wrapperspb.String(string(bytes)) + return nil + } +} + +// WithEntityStartOrchestrationInstanceID sets the instance ID for the new orchestration. +func WithEntityStartOrchestrationInstanceID(instanceID string) entityStartOrchestrationOption { + return func(opts *entityStartOrchestrationOptions) error { + opts.instanceID = instanceID + return nil + } +} + +// callEntityOption is a functional option type for the CallEntity orchestrator method. +type callEntityOption func(*callEntityOptions) error + +type callEntityOptions struct { + rawInput *wrapperspb.StringValue +} + +// WithEntityInput configures an input for an entity operation invocation. +func WithEntityInput(input any) callEntityOption { + return func(opt *callEntityOptions) error { + data, err := marshalData(input) + if err != nil { + return err + } + opt.rawInput = wrapperspb.String(string(data)) + return nil + } +} + +// WithRawEntityInput configures a raw input for an entity operation invocation. +func WithRawEntityInput(input string) callEntityOption { + return func(opt *callEntityOptions) error { + opt.rawInput = wrapperspb.String(input) + return nil + } +} + +// signalEntityOption is a functional option type for the SignalEntity orchestrator method. +type signalEntityOption func(*signalEntityOptions) error + +type signalEntityOptions struct { + rawInput *wrapperspb.StringValue +} + +// WithSignalEntityInput configures an input for a signal entity invocation. +func WithSignalEntityInput(input any) signalEntityOption { + return func(opt *signalEntityOptions) error { + data, err := marshalData(input) + if err != nil { + return err + } + opt.rawInput = wrapperspb.String(string(data)) + return nil + } +} + +// WithRawSignalEntityInput configures a raw input for a signal entity invocation. +func WithRawSignalEntityInput(input string) signalEntityOption { + return func(opt *signalEntityOptions) error { + opt.rawInput = wrapperspb.String(input) + return nil + } +} diff --git a/task/entity_dispatch.go b/task/entity_dispatch.go new file mode 100644 index 00000000..1b45f807 --- /dev/null +++ b/task/entity_dispatch.go @@ -0,0 +1,179 @@ +package task + +import ( + "fmt" + "reflect" + "strings" + + "github.com/microsoft/durabletask-go/api" +) + +// EntityDispatcher provides automatic operation dispatch for entities. +// It maps entity operations to methods on a state struct. +// +// Methods on the state struct are matched by name (case-insensitive). +// Each method can have one of these signatures: +// +// func (s *State) OperationName() (any, error) +// func (s *State) OperationName(input InputType) (any, error) +// func (s *State) OperationName(ctx *EntityContext) (any, error) +// func (s *State) OperationName(ctx *EntityContext, input InputType) (any, error) +// +// Example usage: +// +// type Counter struct { +// Value int `json:"value"` +// } +// +// func (c *Counter) Add(amount int) (any, error) { +// c.Value += amount +// return c.Value, nil +// } +// +// func (c *Counter) Get() (any, error) { +// return c.Value, nil +// } +// +// func (c *Counter) Reset() (any, error) { +// c.Value = 0 +// return nil, nil +// } +// +// // Register entity: +// r.AddEntityN("counter", task.NewEntityFor[Counter]()) + +// NewEntityFor creates an entity function that automatically dispatches +// operations to methods on a state struct of type S. +// +// The state is automatically loaded from and saved to the entity context. +// If the entity has no state, a zero-value S is used. +// +// The special operation "delete" resets the entity state (unless a Delete method exists). +func NewEntityFor[S any]() Entity { + if reflect.TypeFor[S]().Kind() == reflect.Ptr { + panic("NewEntityFor does not support pointer state types") + } + + return func(ctx *EntityContext) (any, error) { + // Load state + var state S + if ctx.HasState() { + if err := ctx.GetState(&state); err != nil { + return nil, fmt.Errorf("failed to deserialize entity state: %w", err) + } + } + + // Handle implicit "delete" operation + if strings.EqualFold(ctx.Operation, "delete") { + // Check if a user-defined Delete method exists first + if _, found := findMethod(reflect.TypeOf(&state), "delete"); !found { + return nil, ctx.SetState(nil) + } + } + + // Dispatch to method + result, err := dispatchToMethod(ctx, &state) + if err != nil { + return nil, err + } + + // Save state back + if !ctx.stateDirty { + if err := ctx.SetState(state); err != nil { + return nil, fmt.Errorf("failed to save entity state: %w", err) + } + } + return result, nil + } +} + +func findMethod(t reflect.Type, name string) (reflect.Method, bool) { + for i := 0; i < t.NumMethod(); i++ { + m := t.Method(i) + if strings.EqualFold(m.Name, name) { + return m, true + } + } + return reflect.Method{}, false +} + +func dispatchToMethod[S any](ctx *EntityContext, state *S) (any, error) { + stateVal := reflect.ValueOf(state) + method, found := findMethod(stateVal.Type(), ctx.Operation) + if !found { + return nil, fmt.Errorf("entity does not support operation '%s'", ctx.Operation) + } + + methodType := method.Type + // numIn includes the receiver + numIn := methodType.NumIn() + + var args []reflect.Value + args = append(args, stateVal) // receiver + + for i := 1; i < numIn; i++ { + paramType := methodType.In(i) + + // Check if it's *EntityContext + if paramType == reflect.TypeOf((*EntityContext)(nil)) { + args = append(args, reflect.ValueOf(ctx)) + continue + } + + // Check if it's api.EntityID + if paramType == reflect.TypeFor[api.EntityID]() { + args = append(args, reflect.ValueOf(ctx.ID)) + continue + } + + // Otherwise, treat as input parameter + inputPtr := reflect.New(paramType) + if err := ctx.GetInput(inputPtr.Interface()); err != nil { + return nil, fmt.Errorf("failed to deserialize input for operation '%s': %w", ctx.Operation, err) + } + args = append(args, inputPtr.Elem()) + } + + results := method.Func.Call(args) + + // Parse return values: expect (any, error) or (error) or () + errorType := reflect.TypeOf((*error)(nil)).Elem() + isNilValue := func(v reflect.Value) bool { + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return v.IsNil() + default: + return false + } + } + + switch len(results) { + case 0: + return nil, nil + case 1: + if results[0].Type().Implements(errorType) { + if isNilValue(results[0]) { + return nil, nil + } + return nil, results[0].Interface().(error) + } + return results[0].Interface(), nil + case 2: + errVal := results[1] + if !errVal.Type().Implements(errorType) { + return nil, fmt.Errorf("method '%s' has unsupported error return type: %s", ctx.Operation, errVal.Type()) + } + + var retErr error + if !isNilValue(errVal) { + retErr = errVal.Interface().(error) + } + + if isNilValue(results[0]) { + return nil, retErr + } + return results[0].Interface(), retErr + default: + return nil, fmt.Errorf("method '%s' has unsupported number of return values: %d", ctx.Operation, len(results)) + } +} diff --git a/task/entity_dispatch_test.go b/task/entity_dispatch_test.go new file mode 100644 index 00000000..15174fec --- /dev/null +++ b/task/entity_dispatch_test.go @@ -0,0 +1,569 @@ +package task + +import ( + "fmt" + "testing" + + "github.com/microsoft/durabletask-go/api" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testCounter struct { + Value int `json:"value"` +} + +func (c *testCounter) Add(amount int) (any, error) { + c.Value += amount + return c.Value, nil +} + +func (c *testCounter) Get() (any, error) { + return c.Value, nil +} + +func (c *testCounter) Reset() (any, error) { + c.Value = 0 + return nil, nil +} + +func Test_EntityDispatcher_BasicOperations(t *testing.T) { + entity := NewEntityFor[testCounter]() + + // Test "Add" operation + ctx := &EntityContext{ + ID: api.NewEntityID("counter", "test"), + Operation: "Add", + rawInput: []byte("5"), + } + result, err := entity(ctx) + require.NoError(t, err) + assert.Equal(t, 5, result) + assert.True(t, ctx.HasState()) + + // Verify state was saved + var state testCounter + require.NoError(t, ctx.GetState(&state)) + assert.Equal(t, 5, state.Value) +} + +func Test_EntityDispatcher_CaseInsensitive(t *testing.T) { + entity := NewEntityFor[testCounter]() + + ctx := &EntityContext{ + ID: api.NewEntityID("counter", "test"), + Operation: "add", // lowercase + rawInput: []byte("10"), + } + result, err := entity(ctx) + require.NoError(t, err) + assert.Equal(t, 10, result) +} + +func Test_EntityDispatcher_WithExistingState(t *testing.T) { + entity := NewEntityFor[testCounter]() + + ctx := &EntityContext{ + ID: api.NewEntityID("counter", "test"), + Operation: "Add", + rawInput: []byte("3"), + state: entityState{value: []byte(`{"value":7}`), hasValue: true}, + } + result, err := entity(ctx) + require.NoError(t, err) + assert.Equal(t, 10, result) +} + +func Test_EntityDispatcher_Get(t *testing.T) { + entity := NewEntityFor[testCounter]() + + ctx := &EntityContext{ + ID: api.NewEntityID("counter", "test"), + Operation: "Get", + state: entityState{value: []byte(`{"value":42}`), hasValue: true}, + } + result, err := entity(ctx) + require.NoError(t, err) + assert.Equal(t, 42, result) +} + +func Test_EntityDispatcher_Reset(t *testing.T) { + entity := NewEntityFor[testCounter]() + + ctx := &EntityContext{ + ID: api.NewEntityID("counter", "test"), + Operation: "Reset", + state: entityState{value: []byte(`{"value":42}`), hasValue: true}, + } + _, err := entity(ctx) + require.NoError(t, err) + + var state testCounter + require.NoError(t, ctx.GetState(&state)) + assert.Equal(t, 0, state.Value) +} + +func Test_EntityDispatcher_ImplicitDelete(t *testing.T) { + entity := NewEntityFor[testCounter]() + + ctx := &EntityContext{ + ID: api.NewEntityID("counter", "test"), + Operation: "delete", + state: entityState{value: []byte(`{"value":42}`), hasValue: true}, + } + _, err := entity(ctx) + require.NoError(t, err) + assert.False(t, ctx.HasState()) +} + +func Test_EntityDispatcher_UnknownOperation(t *testing.T) { + entity := NewEntityFor[testCounter]() + + ctx := &EntityContext{ + ID: api.NewEntityID("counter", "test"), + Operation: "unknown", + } + _, err := entity(ctx) + require.Error(t, err) + assert.Contains(t, err.Error(), "does not support operation") +} + +// Test with EntityContext parameter +type contextAwareEntity struct { + LastOp string `json:"lastOp"` +} + +func (e *contextAwareEntity) Info(ctx *EntityContext) (any, error) { + e.LastOp = ctx.Operation + return ctx.ID.String(), nil +} + +func Test_EntityDispatcher_WithEntityContext(t *testing.T) { + entity := NewEntityFor[contextAwareEntity]() + + ctx := &EntityContext{ + ID: api.NewEntityID("myentity", "key1"), + Operation: "Info", + } + result, err := entity(ctx) + require.NoError(t, err) + assert.Equal(t, "@myentity@key1", result) + + var state contextAwareEntity + require.NoError(t, ctx.GetState(&state)) + assert.Equal(t, "Info", state.LastOp) +} + +// OperationNotSupported_Fails: tests rejection of non-existent methods +func Test_EntityDispatcher_OperationNotSupported(t *testing.T) { + entity := NewEntityFor[testCounter]() + + tests := []struct { + name string + op string + }{ + {"non-existent method", "doesNotExist"}, + {"special chars", "add!"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := &EntityContext{ + ID: api.NewEntityID("counter", "k"), + Operation: tt.op, + } + _, err := entity(ctx) + require.Error(t, err) + assert.Contains(t, err.Error(), "does not support operation") + }) + } +} + +// Add_Success with case-insensitive matching (lowercase, uppercase, mixed) +func Test_EntityDispatcher_CaseInsensitiveMethodMatching(t *testing.T) { + entity := NewEntityFor[testCounter]() + + cases := []string{"add", "Add", "ADD", "aDd"} + for _, op := range cases { + t.Run(op, func(t *testing.T) { + ctx := &EntityContext{ + ID: api.NewEntityID("counter", "k"), + Operation: op, + rawInput: []byte("7"), + } + result, err := entity(ctx) + require.NoError(t, err) + assert.Equal(t, 7, result) + }) + } +} + +// Get_Success: state retrieval from existing state +func Test_EntityDispatcher_GetFromExistingState(t *testing.T) { + entity := NewEntityFor[testCounter]() + + ctx := &EntityContext{ + ID: api.NewEntityID("counter", "k"), + Operation: "Get", + state: entityState{value: []byte(`{"value":42}`), hasValue: true}, + } + result, err := entity(ctx) + require.NoError(t, err) + assert.Equal(t, 42, result) +} + +// Add_NoInput: in Go, missing input for int results in zero value +func Test_EntityDispatcher_MissingInputUsesZeroValue(t *testing.T) { + entity := NewEntityFor[testCounter]() + + ctx := &EntityContext{ + ID: api.NewEntityID("counter", "k"), + Operation: "Add", + // no rawInput — int will default to 0 + } + result, err := entity(ctx) + require.NoError(t, err) + assert.Equal(t, 0, result) // 0 + 0 = 0 +} + +// ImplicitDelete_ClearsState: default delete operation clears state +func Test_EntityDispatcher_ImplicitDeleteClearsState(t *testing.T) { + entity := NewEntityFor[testCounter]() + + // "delete" and "Delete" both work (case-insensitive) + for _, op := range []string{"delete", "Delete", "DELETE"} { + t.Run(op, func(t *testing.T) { + ctx := &EntityContext{ + ID: api.NewEntityID("counter", "k"), + Operation: op, + state: entityState{value: []byte(`{"value":42}`), hasValue: true}, + } + _, err := entity(ctx) + require.NoError(t, err) + assert.False(t, ctx.HasState(), "state should be cleared after delete") + }) + } +} + +// ExplicitDelete_Overridden: custom delete method takes precedence +type entityWithDelete struct { + Value int `json:"value"` + Deleted bool `json:"deleted"` +} + +func (e *entityWithDelete) Delete() (any, error) { + e.Deleted = true + return "custom delete", nil +} + +func (e *entityWithDelete) Get() (any, error) { + return e.Value, nil +} + +func Test_EntityDispatcher_ExplicitDeleteOverridesImplicit(t *testing.T) { + entity := NewEntityFor[entityWithDelete]() + + ctx := &EntityContext{ + ID: api.NewEntityID("e", "k"), + Operation: "delete", + state: entityState{value: []byte(`{"value":42,"deleted":false}`), hasValue: true}, + } + result, err := entity(ctx) + require.NoError(t, err) + assert.Equal(t, "custom delete", result) + // State should still exist (not implicitly cleared) since custom Delete ran + assert.True(t, ctx.HasState()) + + var state entityWithDelete + require.NoError(t, ctx.GetState(&state)) + assert.True(t, state.Deleted) + assert.Equal(t, 42, state.Value) +} + +type entityWithContextDelete struct { + Value int `json:"value"` + Deleted bool `json:"deleted"` +} + +func (e *entityWithContextDelete) Delete(ctx *EntityContext) (any, error) { + e.Deleted = true + return "deleted via ctx", ctx.SetState(nil) +} + +func Test_EntityDispatcher_ExplicitContextDeleteCanClearState(t *testing.T) { + entity := NewEntityFor[entityWithContextDelete]() + + ctx := &EntityContext{ + ID: api.NewEntityID("e", "k"), + Operation: "delete", + state: entityState{value: []byte(`{"value":42,"deleted":false}`), hasValue: true}, + } + result, err := entity(ctx) + require.NoError(t, err) + assert.Equal(t, "deleted via ctx", result) + assert.False(t, ctx.HasState()) +} + +// Throws_ExceptionPreserved: error propagation from entity methods +type errorEntity struct{} + +func (e *errorEntity) Fail() (any, error) { + return nil, fmt.Errorf("entity operation failed: %w", assert.AnError) +} + +func (e *errorEntity) Get() (any, error) { + return "ok", nil +} + +func Test_EntityDispatcher_ErrorPreserved(t *testing.T) { + entity := NewEntityFor[errorEntity]() + + ctx := &EntityContext{ + ID: api.NewEntityID("e", "k"), + Operation: "Fail", + } + _, err := entity(ctx) + require.Error(t, err) + assert.Contains(t, err.Error(), "entity operation failed") + assert.ErrorIs(t, err, assert.AnError) +} + +// State machine pattern: complex state transitions (ported from ExportJobTests) +type jobState struct { + Status string `json:"status"` + Name string `json:"name"` +} + +func (j *jobState) Create(name string) (any, error) { + if j.Status != "" { + return nil, fmt.Errorf("job already exists with status %s", j.Status) + } + j.Status = "active" + j.Name = name + return j.Status, nil +} + +func (j *jobState) Complete() (any, error) { + if j.Status != "active" { + return nil, fmt.Errorf("invalid transition: cannot complete job with status %s", j.Status) + } + j.Status = "completed" + return j.Status, nil +} + +func (j *jobState) Fail() (any, error) { + if j.Status != "active" { + return nil, fmt.Errorf("invalid transition: cannot fail job with status %s", j.Status) + } + j.Status = "failed" + return j.Status, nil +} + +func (j *jobState) Get() (any, error) { + return *j, nil +} + +func Test_EntityDispatcher_StateMachine(t *testing.T) { + entity := NewEntityFor[jobState]() + + t.Run("create then complete", func(t *testing.T) { + ctx := &EntityContext{ + ID: api.NewEntityID("job", "1"), Operation: "Create", + rawInput: []byte(`"myJob"`), + } + result, err := entity(ctx) + require.NoError(t, err) + assert.Equal(t, "active", result) + + ctx2 := &EntityContext{ + ID: api.NewEntityID("job", "1"), Operation: "Complete", + state: ctx.state, + } + result2, err := entity(ctx2) + require.NoError(t, err) + assert.Equal(t, "completed", result2) + }) + + t.Run("complete without create fails", func(t *testing.T) { + ctx := &EntityContext{ + ID: api.NewEntityID("job", "2"), Operation: "Complete", + } + _, err := entity(ctx) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid transition") + }) + + t.Run("create twice fails", func(t *testing.T) { + ctx := &EntityContext{ + ID: api.NewEntityID("job", "3"), Operation: "Create", + rawInput: []byte(`"first"`), + } + _, err := entity(ctx) + require.NoError(t, err) + + ctx2 := &EntityContext{ + ID: api.NewEntityID("job", "3"), Operation: "Create", + rawInput: []byte(`"second"`), state: ctx.state, + } + _, err = entity(ctx2) + require.Error(t, err) + assert.Contains(t, err.Error(), "already exists") + }) + + t.Run("create then fail then recreate", func(t *testing.T) { + ctx := &EntityContext{ + ID: api.NewEntityID("job", "4"), Operation: "Create", + rawInput: []byte(`"v1"`), + } + _, err := entity(ctx) + require.NoError(t, err) + + ctx2 := &EntityContext{ + ID: api.NewEntityID("job", "4"), Operation: "Fail", + state: ctx.state, + } + _, err = entity(ctx2) + require.NoError(t, err) + + // After failure, delete state and re-create + ctx3 := &EntityContext{ + ID: api.NewEntityID("job", "4"), Operation: "delete", + state: ctx2.state, + } + _, err = entity(ctx3) + require.NoError(t, err) + assert.False(t, ctx3.HasState()) + + ctx4 := &EntityContext{ + ID: api.NewEntityID("job", "4"), Operation: "Create", + rawInput: []byte(`"v2"`), + } + result, err := entity(ctx4) + require.NoError(t, err) + assert.Equal(t, "active", result) + }) +} + +// Multiple return type support +type multiReturnEntity struct{} + +func (e *multiReturnEntity) NoReturn() { + // void method +} + +func (e *multiReturnEntity) ErrorOnly() error { + return nil +} + +func (e *multiReturnEntity) ErrorOnlyFail() error { + return fmt.Errorf("error only") +} + +func (e *multiReturnEntity) ResultOnly() any { + return 42 +} + +func Test_EntityDispatcher_ReturnTypeVariations(t *testing.T) { + entity := NewEntityFor[multiReturnEntity]() + + t.Run("void return", func(t *testing.T) { + ctx := &EntityContext{ID: api.NewEntityID("e", "k"), Operation: "NoReturn"} + result, err := entity(ctx) + require.NoError(t, err) + assert.Nil(t, result) + }) + + t.Run("error-only success", func(t *testing.T) { + ctx := &EntityContext{ID: api.NewEntityID("e", "k"), Operation: "ErrorOnly"} + result, err := entity(ctx) + require.NoError(t, err) + assert.Nil(t, result) + }) + + t.Run("error-only failure", func(t *testing.T) { + ctx := &EntityContext{ID: api.NewEntityID("e", "k"), Operation: "ErrorOnlyFail"} + _, err := entity(ctx) + require.Error(t, err) + assert.Contains(t, err.Error(), "error only") + }) + + t.Run("result-only", func(t *testing.T) { + ctx := &EntityContext{ID: api.NewEntityID("e", "k"), Operation: "ResultOnly"} + result, err := entity(ctx) + require.NoError(t, err) + assert.Equal(t, 42, result) + }) +} + +// Entity with context + input parameter binding +type fullBindingEntity struct { + Log []string `json:"log"` +} + +func (e *fullBindingEntity) Process(ctx *EntityContext, msg string) (any, error) { + entry := fmt.Sprintf("%s:%s:%s", ctx.ID.String(), ctx.Operation, msg) + e.Log = append(e.Log, entry) + return len(e.Log), nil +} + +func Test_EntityDispatcher_ContextAndInputBinding(t *testing.T) { + entity := NewEntityFor[fullBindingEntity]() + + ctx := &EntityContext{ + ID: api.NewEntityID("logger", "main"), Operation: "Process", + rawInput: []byte(`"hello world"`), + } + result, err := entity(ctx) + require.NoError(t, err) + assert.Equal(t, 1, result) + + var state fullBindingEntity + require.NoError(t, ctx.GetState(&state)) + require.Len(t, state.Log, 1) + assert.Equal(t, "@logger@main:Process:hello world", state.Log[0]) +} + +type explicitStateEntity struct { + Value int `json:"value"` + Mode string `json:"mode,omitempty"` +} + +func (e *explicitStateEntity) Replace(ctx *EntityContext, value int) (any, error) { + e.Value = value + e.Mode = "receiver" + return value, ctx.SetState(explicitStateEntity{Value: value * 2, Mode: "context"}) +} + +func Test_EntityDispatcher_ExplicitContextStateWins(t *testing.T) { + entity := NewEntityFor[explicitStateEntity]() + + ctx := &EntityContext{ + ID: api.NewEntityID("explicit", "k"), + Operation: "Replace", + rawInput: []byte("5"), + } + result, err := entity(ctx) + require.NoError(t, err) + assert.Equal(t, 5, result) + + var state explicitStateEntity + require.NoError(t, ctx.GetState(&state)) + assert.Equal(t, explicitStateEntity{Value: 10, Mode: "context"}, state) +} + +func Test_EntityDispatcher_RejectsPointerStateType(t *testing.T) { + assert.PanicsWithValue(t, "NewEntityFor does not support pointer state types", func() { + _ = NewEntityFor[*testCounter]() + }) +} + +func Test_EntityDispatcher_ZeroValueInitialization(t *testing.T) { + entity := NewEntityFor[testCounter]() + + // No initial state — should start with zero-value testCounter{Value: 0} + ctx := &EntityContext{ + ID: api.NewEntityID("counter", "new"), Operation: "Get", + } + result, err := entity(ctx) + require.NoError(t, err) + assert.Equal(t, 0, result) // zero-value int +} diff --git a/task/entity_test.go b/task/entity_test.go new file mode 100644 index 00000000..98db4045 --- /dev/null +++ b/task/entity_test.go @@ -0,0 +1,169 @@ +package task + +import ( + "regexp" + "testing" + + "github.com/microsoft/durabletask-go/api" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_EntityContext_State(t *testing.T) { + t.Run("no state initially", func(t *testing.T) { + ctx := &EntityContext{ + ID: api.NewEntityID("test", "key1"), + Operation: "op", + } + assert.False(t, ctx.HasState()) + assert.Error(t, ctx.GetState(new(int))) + }) + + t.Run("set and get state", func(t *testing.T) { + ctx := &EntityContext{ + ID: api.NewEntityID("test", "key1"), + Operation: "op", + } + require.NoError(t, ctx.SetState(42)) + assert.True(t, ctx.HasState()) + + var val int + require.NoError(t, ctx.GetState(&val)) + assert.Equal(t, 42, val) + }) + + t.Run("delete state with nil", func(t *testing.T) { + ctx := &EntityContext{ + ID: api.NewEntityID("test", "key1"), + Operation: "op", + state: entityState{value: []byte("42"), hasValue: true}, + } + assert.True(t, ctx.HasState()) + require.NoError(t, ctx.SetState(nil)) + assert.False(t, ctx.HasState()) + }) + + t.Run("set struct state", func(t *testing.T) { + type MyState struct { + Count int `json:"count"` + Name string `json:"name"` + } + ctx := &EntityContext{ + ID: api.NewEntityID("test", "key1"), + Operation: "op", + } + require.NoError(t, ctx.SetState(MyState{Count: 5, Name: "hello"})) + assert.True(t, ctx.HasState()) + + var result MyState + require.NoError(t, ctx.GetState(&result)) + assert.Equal(t, 5, result.Count) + assert.Equal(t, "hello", result.Name) + }) +} + +func Test_EntityContext_GetInput(t *testing.T) { + ctx := &EntityContext{ + ID: api.NewEntityID("test", "key1"), + Operation: "op", + rawInput: []byte(`"hello"`), + } + + var input string + require.NoError(t, ctx.GetInput(&input)) + assert.Equal(t, "hello", input) +} + +func Test_EntityContext_SignalEntity(t *testing.T) { + ctx := &EntityContext{ + ID: api.NewEntityID("test", "key1"), + Operation: "op", + } + + err := ctx.SignalEntity(api.NewEntityID("other", "key2"), "increment", 5) + require.NoError(t, err) + require.Len(t, ctx.actions, 1) + + action := ctx.actions[0] + signal := action.GetSendSignal() + require.NotNil(t, signal) + assert.Equal(t, "@other@key2", signal.InstanceId) + assert.Equal(t, "increment", signal.Name) + assert.Equal(t, "5", signal.Input.GetValue()) +} + +func Test_EntityContext_SignalEntity_RejectsInvalidEntityID(t *testing.T) { + ctx := &EntityContext{ + ID: api.NewEntityID("test", "key1"), + Operation: "op", + } + + err := ctx.SignalEntity(api.EntityID{Name: "bad@name", Key: "key2"}, "increment", 5) + require.Error(t, err) + require.Empty(t, ctx.actions) +} + +func Test_EntityContext_StartNewOrchestration(t *testing.T) { + ctx := &EntityContext{ + ID: api.NewEntityID("test", "key1"), + Operation: "op", + } + + err := ctx.StartNewOrchestration("MyOrchestrator", + WithEntityStartOrchestrationInput("hello"), + WithEntityStartOrchestrationInstanceID("my-instance"), + ) + require.NoError(t, err) + require.Len(t, ctx.actions, 1) + + action := ctx.actions[0] + startOrch := action.GetStartNewOrchestration() + require.NotNil(t, startOrch) + assert.Equal(t, "MyOrchestrator", startOrch.Name) + assert.Equal(t, "my-instance", startOrch.InstanceId) + assert.Equal(t, `"hello"`, startOrch.Input.GetValue()) +} + +func Test_EntityContext_StartNewOrchestration_DefaultInstanceID(t *testing.T) { + ctx := &EntityContext{ + ID: api.NewEntityID("test", "key1"), + Operation: "op", + } + + err := ctx.StartNewOrchestration("MyOrchestrator") + require.NoError(t, err) + require.Len(t, ctx.actions, 1) + + startOrch := ctx.actions[0].GetStartNewOrchestration() + require.NotNil(t, startOrch) + assert.Regexp(t, regexp.MustCompile("^[a-f0-9]{32}$"), startOrch.InstanceId) +} + +func Test_EntityContext_StartNewOrchestration_RejectsEntityInstanceID(t *testing.T) { + ctx := &EntityContext{ + ID: api.NewEntityID("test", "key1"), + Operation: "op", + } + + err := ctx.StartNewOrchestration("MyOrchestrator", WithEntityStartOrchestrationInstanceID("@counter@key1")) + require.Error(t, err) + require.Empty(t, ctx.actions) +} + +func Test_EntityRegistry(t *testing.T) { + r := NewTaskRegistry() + + myEntity := func(ctx *EntityContext) (any, error) { return nil, nil } + require.NoError(t, r.AddEntityN("counter", myEntity)) + + // Duplicate registration should fail + err := r.AddEntityN("counter", myEntity) + require.Error(t, err) + assert.Contains(t, err.Error(), "already registered") + + err = r.AddEntityN("", myEntity) + require.Error(t, err) + + err = r.AddEntityN("bad@name", myEntity) + require.Error(t, err) +} diff --git a/task/executor.go b/task/executor.go index c210e9be..8eabc85c 100644 --- a/task/executor.go +++ b/task/executor.go @@ -96,6 +96,128 @@ func (te taskExecutor) Shutdown(ctx context.Context) error { return nil } +// ExecuteEntity implements backend.Executor and executes an entity batch in the current goroutine. +func (te *taskExecutor) ExecuteEntity(ctx context.Context, req *protos.EntityBatchRequest) (result *protos.EntityBatchResult, err error) { + entityID, parseErr := api.EntityIDFromString(req.InstanceId) + if parseErr != nil { + return nil, fmt.Errorf("invalid entity instance ID: %w", parseErr) + } + + invoker, ok := te.Registry.entities[entityID.Name] + if !ok { + // try the wildcard match + invoker, ok = te.Registry.entities["*"] + if !ok { + return &protos.EntityBatchResult{ + FailureDetails: &protos.TaskFailureDetails{ + ErrorType: "EntityNotRegistered", + ErrorMessage: fmt.Sprintf("no entity named '%s' was registered", entityID.Name), + }, + }, nil + } + } + + // Initialize entity state from the batch request + var state entityState + if req.EntityState != nil { + state = entityState{ + value: []byte(req.EntityState.GetValue()), + hasValue: true, + } + } + + results := make([]*protos.OperationResult, 0, len(req.Operations)) + var allActions []*protos.OperationAction + + for _, op := range req.Operations { + entityCtx := &EntityContext{ + ID: entityID, + Operation: op.Operation, + rawInput: []byte(op.Input.GetValue()), + state: state, + } + + // Execute the entity function, converting panics to failures + opResult := func() (opResult *protos.OperationResult) { + defer func() { + panicVal := recover() + if panicVal != nil { + opResult = &protos.OperationResult{ + ResultType: &protos.OperationResult_Failure{ + Failure: &protos.OperationResultFailure{ + FailureDetails: &protos.TaskFailureDetails{ + ErrorType: "EntityOperationPanic", + ErrorMessage: fmt.Sprintf("panic: %v", panicVal), + }, + }, + }, + } + } + }() + + output, opErr := invoker(entityCtx) + if opErr != nil { + // Operation failed - rollback state + return &protos.OperationResult{ + ResultType: &protos.OperationResult_Failure{ + Failure: &protos.OperationResultFailure{ + FailureDetails: &protos.TaskFailureDetails{ + ErrorType: fmt.Sprintf("%T", opErr), + ErrorMessage: fmt.Sprintf("%+v", opErr), + }, + }, + }, + } + } + + var rawResult *wrapperspb.StringValue + if output != nil { + bytes, marshalErr := marshalData(output) + if marshalErr != nil { + return &protos.OperationResult{ + ResultType: &protos.OperationResult_Failure{ + Failure: &protos.OperationResultFailure{ + FailureDetails: &protos.TaskFailureDetails{ + ErrorType: fmt.Sprintf("%T", marshalErr), + ErrorMessage: fmt.Sprintf("failed to marshal entity result: %+v", marshalErr), + }, + }, + }, + } + } + if len(bytes) > 0 { + rawResult = wrapperspb.String(string(bytes)) + } + } + + // Only commit state after successful marshal + state = entityCtx.state + allActions = append(allActions, entityCtx.actions...) + + return &protos.OperationResult{ + ResultType: &protos.OperationResult_Success{ + Success: &protos.OperationResultSuccess{ + Result: rawResult, + }, + }, + } + }() + + results = append(results, opResult) + } + + batchResult := &protos.EntityBatchResult{ + Results: results, + Actions: allActions, + } + + if state.hasValue { + batchResult.EntityState = wrapperspb.String(string(state.value)) + } + + return batchResult, nil +} + func unmarshalData(data []byte, v any) error { switch { case v == nil: diff --git a/task/orchestrator.go b/task/orchestrator.go index 8afc3ac6..f3b167b8 100644 --- a/task/orchestrator.go +++ b/task/orchestrator.go @@ -11,6 +11,7 @@ import ( "google.golang.org/protobuf/types/known/wrapperspb" + "github.com/google/uuid" "github.com/microsoft/durabletask-go/api" "github.com/microsoft/durabletask-go/backend" "github.com/microsoft/durabletask-go/internal/helpers" @@ -40,6 +41,7 @@ type OrchestrationContext struct { continuedAsNew bool continuedAsNewInput any customStatus string + newGuidCounter int bufferedExternalEvents map[string]*list.List pendingExternalEventTasks map[string]*list.List @@ -227,6 +229,8 @@ func (ctx *OrchestrationContext) processEvent(e *backend.HistoryEvent) error { err = ctx.onExecutionResumed(er) } else if et := e.GetExecutionTerminated(); et != nil { err = ctx.onExecutionTerminated(et) + } else if ev := e.GetEventSent(); ev != nil { + err = ctx.onEventSent(e.EventId, ev) } else if oc := e.GetOrchestratorCompleted(); oc != nil { // Nothing to do } else { @@ -239,6 +243,23 @@ func (octx *OrchestrationContext) SetCustomStatus(cs string) { octx.customStatus = cs } +// guidNamespace UUID matching the .NET SDK's DnsNamespaceValue +var guidNamespace = uuid.MustParse("9e952958-5e33-4daf-827f-2fa12937b875") + +// NewGuid generates a deterministic UUID v5 that is safe for use in orchestrator functions. +// The generated UUID is based on the orchestration instance ID, the current orchestrator +// timestamp, and an internal counter, making it deterministic across replays. +// +// This is compatible with the .NET Durable Task SDK's NewGuid implementation. +func (ctx *OrchestrationContext) NewGuid() string { + // Format: instanceID_timestamp_counter + // The timestamp uses .NET's 'o' format (7 fractional digits) + ts := ctx.CurrentTimeUtc.UTC().Format("2006-01-02T15:04:05.0000000Z") + name := fmt.Sprintf("%s_%s_%d", ctx.ID, ts, ctx.newGuidCounter) + ctx.newGuidCounter++ + return uuid.NewSHA1(guidNamespace, []byte(name)).String() +} + // GetInput unmarshals the serialized orchestration input and stores it in [v]. func (octx *OrchestrationContext) GetInput(v any) error { return unmarshalData(octx.rawInput, v) @@ -429,6 +450,119 @@ func (ctx *OrchestrationContext) WaitForSingleEvent(eventName string, timeout ti return task } +// CallEntity sends an operation request to an entity and waits for a response. +// The [entityID] parameter identifies the target entity. The [operationName] parameter +// specifies the operation to invoke on the entity. +// +// This method returns a [Task] that completes when the entity operation finishes. +// The result of the entity operation can be obtained by calling [Await] on the returned task. +func (ctx *OrchestrationContext) CallEntity(entityID api.EntityID, operationName string, opts ...callEntityOption) Task { + options := new(callEntityOptions) + for _, configure := range opts { + if err := configure(options); err != nil { + failedTask := newTask(ctx) + failedTask.fail(helpers.NewTaskFailureDetails(err)) + return failedTask + } + } + if err := helpers.ValidateEntityName(entityID.Name); err != nil { + failedTask := newTask(ctx) + failedTask.fail(helpers.NewTaskFailureDetails(err)) + return failedTask + } + + // Generate a deterministic request ID for response correlation. + requestID := ctx.NewGuid() + + // Build the .NET-compatible RequestMessage payload. + reqMsg := helpers.EntityRequestMessage{ + ID: requestID, + ParentInstanceID: string(ctx.ID), + IsSignal: false, + Operation: operationName, + } + if options.rawInput != nil { + reqMsg.Input = options.rawInput.GetValue() + } + payload, err := json.Marshal(reqMsg) + if err != nil { + failedTask := newTask(ctx) + failedTask.fail(helpers.NewTaskFailureDetails(err)) + return failedTask + } + + sendEventAction := helpers.NewSendEventAction( + entityID.String(), + helpers.EntityRequestEventName, + wrapperspb.String(string(payload)), + ) + sendEventAction.Id = ctx.getNextSequenceNumber() + ctx.pendingActions[sendEventAction.Id] = sendEventAction + + // Wait for the entity's response via an external event keyed on the request ID. + // Wrap the task to unwrap the ResponseMessage payload. + return &entityResponseTask{delegate: ctx.WaitForSingleEvent(requestID, -1)} +} + +// SignalEntity sends a fire-and-forget signal to an entity. +// The [entityID] parameter identifies the target entity. The [operationName] parameter +// specifies the operation to invoke on the entity. +// +// Unlike [CallEntity], this method does not wait for a response. The signal is +// processed asynchronously by the target entity. +func (ctx *OrchestrationContext) SignalEntity(entityID api.EntityID, operationName string, opts ...signalEntityOption) error { + options := new(signalEntityOptions) + for _, configure := range opts { + if err := configure(options); err != nil { + return err + } + } + if err := helpers.ValidateEntityName(entityID.Name); err != nil { + return err + } + + // Build the .NET-compatible RequestMessage payload with isSignal=true. + reqMsg := helpers.EntityRequestMessage{ + ID: ctx.NewGuid(), + ParentInstanceID: string(ctx.ID), + IsSignal: true, + Operation: operationName, + } + if options.rawInput != nil { + reqMsg.Input = options.rawInput.GetValue() + } + payload, err := json.Marshal(reqMsg) + if err != nil { + return fmt.Errorf("failed to marshal signal request: %w", err) + } + + sendEventAction := helpers.NewSendEventAction( + entityID.String(), + helpers.EntityRequestEventName, + wrapperspb.String(string(payload)), + ) + sendEventAction.Id = ctx.getNextSequenceNumber() + ctx.pendingActions[sendEventAction.Id] = sendEventAction + return nil +} + +// LockEntities acquires locks on the specified entities, ensuring exclusive access. +// The locks are acquired by sending lock-acquisition events to each entity and waiting +// for confirmation. The returned unlock function must be called to release the locks. +// +// While holding locks, the orchestration is in a "critical section" and can safely +// call entity operations without the risk of conflicts from other orchestrations. +// +// Example usage: +// +// unlock, err := ctx.LockEntities(entityID1, entityID2) +// if err != nil { return nil, err } +// defer unlock() +// // ... perform entity operations safely ... +func (ctx *OrchestrationContext) LockEntities(entityIDs ...api.EntityID) (unlock func(), err error) { + return nil, fmt.Errorf("entity locking is not yet fully implemented: requires backend-level entity scheduler support") +} + func (ctx *OrchestrationContext) ContinueAsNew(newInput any, options ...ContinueAsNewOption) { ctx.continuedAsNew = true ctx.continuedAsNewInput = newInput @@ -594,6 +728,18 @@ func (ctx *OrchestrationContext) onTimerFired(tf *protos.TimerFiredEvent) error return nil } +func (ctx *OrchestrationContext) onEventSent(eventID int32, es *protos.EventSentEvent) error { + if a, ok := ctx.pendingActions[eventID]; !ok || a.GetSendEvent() == nil { + return fmt.Errorf( + "a previous execution sent an event to '%s' with sequence number %d at this point in the orchestration logic, but the current execution doesn't have this action with this sequence number", + es.InstanceId, + eventID, + ) + } + delete(ctx.pendingActions, eventID) + return nil +} + func (ctx *OrchestrationContext) onExternalEventRaised(e *protos.HistoryEvent) error { er := e.GetEventRaised() key := strings.ToUpper(er.GetName()) @@ -729,3 +875,33 @@ func (ctx *OrchestrationContext) actions() []*protos.OrchestratorAction { } return actions } + +// entityResponseTask wraps a task to unwrap entity response payloads. +type entityResponseTask struct { + delegate Task +} + +func (t *entityResponseTask) Await(v any) error { + var resp helpers.EntityResponseMessage + if err := t.delegate.Await(&resp); err != nil { + return err + } + if resp.ErrorMessage != "" { + return &taskFailedError{ErrorMessage: resp.ErrorMessage} + } + if v != nil && resp.Result != "" { + if err := unmarshalData([]byte(resp.Result), v); err != nil { + return err + } + } + return nil +} + +// taskFailedError represents a failure returned by an entity operation. +type taskFailedError struct { + ErrorMessage string +} + +func (e *taskFailedError) Error() string { + return e.ErrorMessage +} diff --git a/task/orchestrator_test.go b/task/orchestrator_test.go index f4842b53..f7608532 100644 --- a/task/orchestrator_test.go +++ b/task/orchestrator_test.go @@ -1,8 +1,15 @@ package task import ( + "encoding/json" "testing" "time" + + "github.com/microsoft/durabletask-go/api" + "github.com/microsoft/durabletask-go/internal/helpers" + "github.com/microsoft/durabletask-go/internal/protos" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_computeNextDelay(t *testing.T) { @@ -131,3 +138,70 @@ func Test_computeNextDelay(t *testing.T) { }) } } + +func Test_NewGuid_Deterministic(t *testing.T) { + ctx := &OrchestrationContext{ + ID: "test-instance-123", + CurrentTimeUtc: time.Date(2024, 1, 15, 10, 30, 45, 0, time.UTC), + } + + // Generate two GUIDs and verify they're different + guid1 := ctx.NewGuid() + guid2 := ctx.NewGuid() + if guid1 == guid2 { + t.Errorf("expected different GUIDs, got same: %s", guid1) + } + + // Verify determinism by resetting the counter + ctx.newGuidCounter = 0 + guid1Again := ctx.NewGuid() + if guid1 != guid1Again { + t.Errorf("expected deterministic GUID, got %s vs %s", guid1, guid1Again) + } +} + +func Test_NewGuid_Format(t *testing.T) { + ctx := &OrchestrationContext{ + ID: "test-instance", + CurrentTimeUtc: time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC), + } + + guid := ctx.NewGuid() + // UUID format: 8-4-4-4-12 hex chars + if len(guid) != 36 { + t.Errorf("expected UUID length 36, got %d: %s", len(guid), guid) + } + if guid[8] != '-' || guid[13] != '-' || guid[18] != '-' || guid[23] != '-' { + t.Errorf("expected UUID format with dashes, got: %s", guid) + } + // Version should be 5 (char at position 14) + if guid[14] != '5' { + t.Errorf("expected version 5 at position 14, got: %c in %s", guid[14], guid) + } +} + +func Test_OrchestrationContext_SignalEntity_SetsParentInstanceID(t *testing.T) { + ctx := &OrchestrationContext{ + ID: api.InstanceID("orchestrator-instance"), + pendingActions: make(map[int32]*protos.OrchestratorAction), + } + + err := ctx.SignalEntity(api.NewEntityID("counter", "key1"), "increment") + require.NoError(t, err) + require.Len(t, ctx.pendingActions, 1) + + var action *protos.OrchestratorAction + for _, candidate := range ctx.pendingActions { + action = candidate + } + require.NotNil(t, action) + + send := action.GetSendEvent() + require.NotNil(t, send) + + var msg helpers.EntityRequestMessage + require.NoError(t, json.Unmarshal([]byte(send.Data.GetValue()), &msg)) + assert.Equal(t, "orchestrator-instance", msg.ParentInstanceID) + assert.True(t, msg.IsSignal) + assert.Equal(t, "increment", msg.Operation) +} diff --git a/task/registry.go b/task/registry.go index f3469dc2..d7830614 100644 --- a/task/registry.go +++ b/task/registry.go @@ -2,14 +2,16 @@ package task import ( "fmt" + "strings" "github.com/microsoft/durabletask-go/internal/helpers" ) -// TaskRegistry contains maps of names to corresponding orchestrator and activity functions. +// TaskRegistry contains maps of names to corresponding orchestrator, activity, and entity functions. type TaskRegistry struct { orchestrators map[string]Orchestrator activities map[string]Activity + entities map[string]Entity } // NewTaskRegistry returns a new [TaskRegistry] struct. @@ -17,6 +19,7 @@ func NewTaskRegistry() *TaskRegistry { r := &TaskRegistry{ orchestrators: make(map[string]Orchestrator), activities: make(map[string]Activity), + entities: make(map[string]Entity), } return r } @@ -52,3 +55,23 @@ func (r *TaskRegistry) AddActivityN(name string, a Activity) error { r.activities[name] = a return nil } + +// AddEntity adds an entity function to the registry. The name of the entity +// function is determined using reflection. +func (r *TaskRegistry) AddEntity(e Entity) error { + name := helpers.GetTaskFunctionName(e) + return r.AddEntityN(name, e) +} + +// AddEntityN adds an entity function to the registry with a specified name. +func (r *TaskRegistry) AddEntityN(name string, e Entity) error { + if err := helpers.ValidateEntityName(name); err != nil { + return err + } + name = strings.ToLower(name) + if _, ok := r.entities[name]; ok { + return fmt.Errorf("entity named '%s' is already registered", name) + } + r.entities[name] = e + return nil +} diff --git a/tests/entity_client_test.go b/tests/entity_client_test.go new file mode 100644 index 00000000..9373e9cd --- /dev/null +++ b/tests/entity_client_test.go @@ -0,0 +1,288 @@ +// Tests for entity client operations, ported from the .NET SDK's ShimDurableEntityClientTests. +package tests + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/microsoft/durabletask-go/api" + "github.com/microsoft/durabletask-go/backend" + "github.com/microsoft/durabletask-go/internal/protos" + "github.com/microsoft/durabletask-go/tests/mocks" +) + +func newEntityClient(be backend.Backend) backend.EntityTaskHubClient { + return backend.NewTaskHubClient(be).(backend.EntityTaskHubClient) +} + +func Test_EntityClient_CleanEntityStorage_Defaults(t *testing.T) { + be := &mocks.EntityBackend{} + client := newEntityClient(be) + ctx := context.Background() + + req := api.CleanEntityStorageRequest{ + RemoveEmptyEntities: true, + ReleaseOrphanedLocks: true, + } + + be.EXPECT().CleanEntityStorage(ctx, req).Return(&api.CleanEntityStorageResult{ + EmptyEntitiesRemoved: 5, + OrphanedLocksReleased: 3, + }, nil).Once() + + result, err := client.CleanEntityStorage(ctx, req) + require.NoError(t, err) + assert.Equal(t, int32(5), result.EmptyEntitiesRemoved) + assert.Equal(t, int32(3), result.OrphanedLocksReleased) + assert.Empty(t, result.ContinuationToken) +} + +func Test_EntityClient_CleanEntityStorage_WithOptions(t *testing.T) { + tests := []struct { + name string + removeEmpty bool + releaseLocks bool + hasContinuationToken bool + }{ + {"remove only", true, false, false}, + {"release only", false, true, false}, + {"both", true, true, false}, + {"with continuation", true, true, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + be := &mocks.EntityBackend{} + client := newEntityClient(be) + ctx := context.Background() + + req := api.CleanEntityStorageRequest{ + RemoveEmptyEntities: tt.removeEmpty, + ReleaseOrphanedLocks: tt.releaseLocks, + } + if tt.hasContinuationToken { + req.ContinuationToken = "token123" + } + + expectedResult := &api.CleanEntityStorageResult{ + EmptyEntitiesRemoved: 2, + OrphanedLocksReleased: 1, + } + if tt.hasContinuationToken { + expectedResult.ContinuationToken = "nextToken" + } + + be.EXPECT().CleanEntityStorage(ctx, req).Return(expectedResult, nil).Once() + + result, err := client.CleanEntityStorage(ctx, req) + require.NoError(t, err) + assert.Equal(t, expectedResult.EmptyEntitiesRemoved, result.EmptyEntitiesRemoved) + assert.Equal(t, expectedResult.OrphanedLocksReleased, result.OrphanedLocksReleased) + assert.Equal(t, expectedResult.ContinuationToken, result.ContinuationToken) + }) + } +} + +func Test_EntityClient_FetchEntityMetadata(t *testing.T) { + tests := []struct { + name string + includeState bool + }{ + {"with state", true}, + {"without state", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + be := &mocks.EntityBackend{} + client := newEntityClient(be) + ctx := context.Background() + + entityID := api.NewEntityID("counter", "myCounter") + now := time.Now().Truncate(time.Second) + + expected := &api.EntityMetadata{ + InstanceID: entityID, + LastModifiedTime: now, + BacklogQueueSize: 2, + LockedBy: "some-orchestration", + } + if tt.includeState { + expected.SerializedState = `{"value":42}` + } + + be.EXPECT().GetEntityMetadata(ctx, entityID, tt.includeState).Return(expected, nil).Once() + + result, err := client.FetchEntityMetadata(ctx, entityID, tt.includeState) + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, entityID, result.InstanceID) + assert.Equal(t, now, result.LastModifiedTime) + assert.Equal(t, int32(2), result.BacklogQueueSize) + assert.Equal(t, "some-orchestration", result.LockedBy) + if tt.includeState { + assert.Equal(t, `{"value":42}`, result.SerializedState) + } + }) + } +} + +func Test_EntityClient_FetchEntityMetadata_NotFound(t *testing.T) { + be := &mocks.EntityBackend{} + client := newEntityClient(be) + ctx := context.Background() + + entityID := api.NewEntityID("counter", "missing") + be.EXPECT().GetEntityMetadata(ctx, entityID, true).Return(nil, nil).Once() + + result, err := client.FetchEntityMetadata(ctx, entityID, true) + require.NoError(t, err) + assert.Nil(t, result) +} + +func Test_EntityClient_QueryEntities_NoFilter(t *testing.T) { + be := &mocks.EntityBackend{} + client := newEntityClient(be) + ctx := context.Background() + + query := api.EntityQuery{IncludeState: true} + + be.EXPECT().QueryEntities(ctx, query).Return(&api.EntityQueryResults{ + Entities: []*api.EntityMetadata{ + {InstanceID: api.NewEntityID("counter", "a"), SerializedState: `1`}, + {InstanceID: api.NewEntityID("counter", "b"), SerializedState: `2`}, + {InstanceID: api.NewEntityID("counter", "c"), SerializedState: `3`}, + }, + }, nil).Once() + + result, err := client.QueryEntities(ctx, query) + require.NoError(t, err) + require.Len(t, result.Entities, 3) + assert.Equal(t, "a", result.Entities[0].InstanceID.Key) + assert.Equal(t, "b", result.Entities[1].InstanceID.Key) + assert.Equal(t, "c", result.Entities[2].InstanceID.Key) +} + +func Test_EntityClient_QueryEntities_WithFilter(t *testing.T) { + be := &mocks.EntityBackend{} + client := newEntityClient(be) + ctx := context.Background() + + now := time.Now().Truncate(time.Second) + query := api.EntityQuery{ + InstanceIDStartsWith: "@counter@", + LastModifiedFrom: now.Add(-1 * time.Hour), + LastModifiedTo: now, + IncludeState: true, + IncludeTransient: false, + PageSize: 10, + ContinuationToken: "page1", + } + + be.EXPECT().QueryEntities(ctx, query).Return(&api.EntityQueryResults{ + Entities: []*api.EntityMetadata{ + {InstanceID: api.NewEntityID("counter", "x"), SerializedState: `99`}, + }, + ContinuationToken: "page2", + }, nil).Once() + + result, err := client.QueryEntities(ctx, query) + require.NoError(t, err) + require.Len(t, result.Entities, 1) + assert.Equal(t, "x", result.Entities[0].InstanceID.Key) + assert.Equal(t, "page2", result.ContinuationToken) +} + +// Tests that backend without EntityBackend returns errors for query/clean operations +func Test_EntityClient_NonEntityBackend_Fallbacks(t *testing.T) { + be := &mocks.Backend{} + client := newEntityClient(be) + ctx := context.Background() + + t.Run("QueryEntities returns error", func(t *testing.T) { + _, err := client.QueryEntities(ctx, api.EntityQuery{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "EntityBackend") + }) + + t.Run("CleanEntityStorage returns error", func(t *testing.T) { + _, err := client.CleanEntityStorage(ctx, api.CleanEntityStorageRequest{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "EntityBackend") + }) + + t.Run("FetchEntityMetadata falls back to orchestration metadata", func(t *testing.T) { + entityID := api.NewEntityID("counter", "fallback") + now := time.Now().Truncate(time.Second) + + be.EXPECT().GetOrchestrationMetadata(ctx, api.InstanceID("@counter@fallback")).Return( + &api.OrchestrationMetadata{ + InstanceID: api.InstanceID("@counter@fallback"), + LastUpdatedAt: now, + SerializedCustomStatus: `{"value":7}`, + }, nil).Once() + + result, err := client.FetchEntityMetadata(ctx, entityID, true) + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, entityID, result.InstanceID) + assert.Equal(t, now, result.LastModifiedTime) + assert.Equal(t, `{"value":7}`, result.SerializedState) + }) +} + +func Test_EntityClient_SignalEntity(t *testing.T) { + be := &mocks.Backend{} + client := newEntityClient(be) + ctx := context.Background() + + entityID := api.NewEntityID("counter", "signalTest") + + // SignalEntity auto-creates the entity instance, then sends the event + be.EXPECT().CreateOrchestrationInstance( + ctx, + mock.AnythingOfType("*protos.HistoryEvent"), + mock.AnythingOfType("backend.OrchestrationIdReusePolicyOptions"), + ).Return(nil).Once() + + be.EXPECT().AddNewOrchestrationEvent( + ctx, + api.InstanceID("@counter@signalTest"), + mock.AnythingOfType("*protos.HistoryEvent"), + ).Return(nil).Once() + + err := client.SignalEntity(ctx, entityID, "increment", api.WithSignalInput(5)) + require.NoError(t, err) +} + +func Test_EntityClient_SignalEntity_PreservesScheduledTime(t *testing.T) { + be := &mocks.Backend{} + client := newEntityClient(be) + ctx := context.Background() + + entityID := api.NewEntityID("counter", "signalTest") + scheduledTime := time.Now().Add(2 * time.Hour).UTC().Truncate(time.Millisecond) + + be.EXPECT().CreateOrchestrationInstance( + ctx, + mock.AnythingOfType("*protos.HistoryEvent"), + mock.AnythingOfType("backend.OrchestrationIdReusePolicyOptions"), + ).Return(nil).Once() + + be.EXPECT().AddNewOrchestrationEvent( + ctx, + api.InstanceID("@counter@signalTest"), + mock.AnythingOfType("*protos.HistoryEvent"), + ).Run(func(_ context.Context, _ api.InstanceID, e *protos.HistoryEvent) { + require.NotNil(t, e.Timestamp) + require.WithinDuration(t, scheduledTime, e.Timestamp.AsTime(), time.Millisecond) + }).Return(nil).Once() + + err := client.SignalEntity(ctx, entityID, "increment", api.WithSignalScheduledTime(scheduledTime)) + require.NoError(t, err) +} diff --git a/tests/entity_executor_test.go b/tests/entity_executor_test.go new file mode 100644 index 00000000..481841ee --- /dev/null +++ b/tests/entity_executor_test.go @@ -0,0 +1,604 @@ +package tests + +import ( + "context" + "fmt" + "testing" + + "github.com/microsoft/durabletask-go/api" + "github.com/microsoft/durabletask-go/backend" + "github.com/microsoft/durabletask-go/internal/protos" + "github.com/microsoft/durabletask-go/task" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +func newEntityExecutor(r *task.TaskRegistry) backend.EntityExecutor { + return task.NewTaskExecutor(r).(backend.EntityExecutor) +} + +func Test_Executor_EntityBasicOperation(t *testing.T) { + r := task.NewTaskRegistry() + require.NoError(t, r.AddEntityN("counter", func(ctx *task.EntityContext) (any, error) { + var count int + if ctx.HasState() { + if err := ctx.GetState(&count); err != nil { + return nil, err + } + } + + switch ctx.Operation { + case "add": + var amount int + if err := ctx.GetInput(&amount); err != nil { + return nil, err + } + count += amount + case "get": + return count, ctx.SetState(count) + case "reset": + count = 0 + } + + if err := ctx.SetState(count); err != nil { + return nil, err + } + return count, nil + })) + + executor := newEntityExecutor(r) + entityCtx := context.Background() + + // Test "add" operation with no initial state + req := &protos.EntityBatchRequest{ + InstanceId: "@counter@myCounter", + Operations: []*protos.OperationRequest{ + { + Operation: "add", + RequestId: "req1", + Input: wrapperspb.String("5"), + }, + }, + } + + result, err := executor.ExecuteEntity(entityCtx, req) + require.NoError(t, err) + require.Len(t, result.Results, 1) + require.NotNil(t, result.Results[0].GetSuccess()) + assert.Equal(t, "5", result.Results[0].GetSuccess().GetResult().GetValue()) + assert.Equal(t, "5", result.EntityState.GetValue()) + + // Test "add" again with existing state + req2 := &protos.EntityBatchRequest{ + InstanceId: "@counter@myCounter", + EntityState: result.EntityState, + Operations: []*protos.OperationRequest{ + { + Operation: "add", + RequestId: "req2", + Input: wrapperspb.String("3"), + }, + }, + } + + result2, err := executor.ExecuteEntity(entityCtx, req2) + require.NoError(t, err) + require.Len(t, result2.Results, 1) + require.NotNil(t, result2.Results[0].GetSuccess()) + assert.Equal(t, "8", result2.Results[0].GetSuccess().GetResult().GetValue()) + assert.Equal(t, "8", result2.EntityState.GetValue()) + + // Test "get" operation + req3 := &protos.EntityBatchRequest{ + InstanceId: "@counter@myCounter", + EntityState: result2.EntityState, + Operations: []*protos.OperationRequest{ + { + Operation: "get", + RequestId: "req3", + }, + }, + } + + result3, err := executor.ExecuteEntity(entityCtx, req3) + require.NoError(t, err) + require.Len(t, result3.Results, 1) + require.NotNil(t, result3.Results[0].GetSuccess()) + assert.Equal(t, "8", result3.Results[0].GetSuccess().GetResult().GetValue()) +} + +func Test_Executor_EntityBatchOperations(t *testing.T) { + r := task.NewTaskRegistry() + require.NoError(t, r.AddEntityN("counter", func(ctx *task.EntityContext) (any, error) { + var count int + if ctx.HasState() { + if err := ctx.GetState(&count); err != nil { + return nil, err + } + } + + switch ctx.Operation { + case "add": + var amount int + if err := ctx.GetInput(&amount); err != nil { + return nil, err + } + count += amount + case "get": + return count, ctx.SetState(count) + } + + if err := ctx.SetState(count); err != nil { + return nil, err + } + return count, nil + })) + + executor := newEntityExecutor(r) + entityCtx := context.Background() + + // Batch multiple operations + req := &protos.EntityBatchRequest{ + InstanceId: "@counter@myCounter", + Operations: []*protos.OperationRequest{ + {Operation: "add", RequestId: "req1", Input: wrapperspb.String("10")}, + {Operation: "add", RequestId: "req2", Input: wrapperspb.String("20")}, + {Operation: "get", RequestId: "req3"}, + }, + } + + result, err := executor.ExecuteEntity(entityCtx, req) + require.NoError(t, err) + require.Len(t, result.Results, 3) + + // First add: 0 + 10 = 10 + require.NotNil(t, result.Results[0].GetSuccess()) + assert.Equal(t, "10", result.Results[0].GetSuccess().GetResult().GetValue()) + + // Second add: 10 + 20 = 30 + require.NotNil(t, result.Results[1].GetSuccess()) + assert.Equal(t, "30", result.Results[1].GetSuccess().GetResult().GetValue()) + + // Get: 30 + require.NotNil(t, result.Results[2].GetSuccess()) + assert.Equal(t, "30", result.Results[2].GetSuccess().GetResult().GetValue()) + + // Final state should be 30 + assert.Equal(t, "30", result.EntityState.GetValue()) +} + +func Test_Executor_EntityOperationError(t *testing.T) { + r := task.NewTaskRegistry() + require.NoError(t, r.AddEntityN("faulty", func(ctx *task.EntityContext) (any, error) { + var count int + if ctx.HasState() { + _ = ctx.GetState(&count) + } + + switch ctx.Operation { + case "fail": + return nil, assert.AnError + case "add": + count++ + _ = ctx.SetState(count) + return count, nil + } + return nil, nil + })) + + executor := newEntityExecutor(r) + entityCtx := context.Background() + + // Batch: add, fail, add — the "fail" operation should not affect state + req := &protos.EntityBatchRequest{ + InstanceId: "@faulty@key1", + Operations: []*protos.OperationRequest{ + {Operation: "add", RequestId: "req1"}, + {Operation: "fail", RequestId: "req2"}, + {Operation: "add", RequestId: "req3"}, + }, + } + + result, err := executor.ExecuteEntity(entityCtx, req) + require.NoError(t, err) + require.Len(t, result.Results, 3) + + // First add succeeds + require.NotNil(t, result.Results[0].GetSuccess()) + assert.Equal(t, "1", result.Results[0].GetSuccess().GetResult().GetValue()) + + // Second op fails + require.NotNil(t, result.Results[1].GetFailure()) + + // Third add succeeds (state from first op is preserved, failure is rolled back) + require.NotNil(t, result.Results[2].GetSuccess()) + assert.Equal(t, "2", result.Results[2].GetSuccess().GetResult().GetValue()) + + // Final state is 2 + assert.Equal(t, "2", result.EntityState.GetValue()) +} + +func Test_Executor_EntityPanic(t *testing.T) { + r := task.NewTaskRegistry() + require.NoError(t, r.AddEntityN("panicky", func(ctx *task.EntityContext) (any, error) { + panic("oh no!") + })) + + executor := newEntityExecutor(r) + entityCtx := context.Background() + + req := &protos.EntityBatchRequest{ + InstanceId: "@panicky@key1", + Operations: []*protos.OperationRequest{ + {Operation: "test", RequestId: "req1"}, + }, + } + + result, err := executor.ExecuteEntity(entityCtx, req) + require.NoError(t, err) + require.Len(t, result.Results, 1) + require.NotNil(t, result.Results[0].GetFailure()) + assert.Contains(t, result.Results[0].GetFailure().GetFailureDetails().GetErrorMessage(), "oh no!") +} + +func Test_Executor_EntityNotRegistered(t *testing.T) { + r := task.NewTaskRegistry() + executor := newEntityExecutor(r) + entityCtx := context.Background() + + req := &protos.EntityBatchRequest{ + InstanceId: "@unknown@key1", + Operations: []*protos.OperationRequest{ + {Operation: "test", RequestId: "req1"}, + }, + } + + result, err := executor.ExecuteEntity(entityCtx, req) + require.NoError(t, err) + require.NotNil(t, result.FailureDetails) + assert.Equal(t, "EntityNotRegistered", result.FailureDetails.ErrorType) +} + +func Test_Executor_EntitySignalAction(t *testing.T) { + r := task.NewTaskRegistry() + require.NoError(t, r.AddEntityN("sender", func(ctx *task.EntityContext) (any, error) { + return nil, ctx.SignalEntity(api.NewEntityID("receiver", "key2"), "notify", "hello") + })) + + executor := newEntityExecutor(r) + entityCtx := context.Background() + + req := &protos.EntityBatchRequest{ + InstanceId: "@sender@key1", + Operations: []*protos.OperationRequest{ + {Operation: "send", RequestId: "req1"}, + }, + } + + result, err := executor.ExecuteEntity(entityCtx, req) + require.NoError(t, err) + require.Len(t, result.Results, 1) + require.NotNil(t, result.Results[0].GetSuccess()) + + // Check that a signal action was emitted + require.Len(t, result.Actions, 1) + signal := result.Actions[0].GetSendSignal() + require.NotNil(t, signal) + assert.Equal(t, "@receiver@key2", signal.InstanceId) + assert.Equal(t, "notify", signal.Name) + assert.Equal(t, `"hello"`, signal.Input.GetValue()) +} + +func Test_Executor_EntityDeleteState(t *testing.T) { + r := task.NewTaskRegistry() + require.NoError(t, r.AddEntityN("deletable", func(ctx *task.EntityContext) (any, error) { + switch ctx.Operation { + case "set": + return nil, ctx.SetState(42) + case "delete": + return nil, ctx.SetState(nil) + } + return nil, nil + })) + + executor := newEntityExecutor(r) + entityCtx := context.Background() + + // First set state + req := &protos.EntityBatchRequest{ + InstanceId: "@deletable@key1", + Operations: []*protos.OperationRequest{ + {Operation: "set", RequestId: "req1"}, + }, + } + + result, err := executor.ExecuteEntity(entityCtx, req) + require.NoError(t, err) + assert.Equal(t, "42", result.EntityState.GetValue()) + + // Then delete state + req2 := &protos.EntityBatchRequest{ + InstanceId: "@deletable@key1", + EntityState: result.EntityState, + Operations: []*protos.OperationRequest{ + {Operation: "delete", RequestId: "req2"}, + }, + } + + result2, err := executor.ExecuteEntity(entityCtx, req2) + require.NoError(t, err) + assert.Nil(t, result2.EntityState) +} + +// Tests that state persists correctly across multiple batch requests +func Test_Executor_EntityStatePersistsAcrossBatches(t *testing.T) { + r := task.NewTaskRegistry() + require.NoError(t, r.AddEntityN("counter", func(ctx *task.EntityContext) (any, error) { + var count int + if ctx.HasState() { + _ = ctx.GetState(&count) + } + switch ctx.Operation { + case "increment": + count++ + case "get": + // no-op + } + _ = ctx.SetState(count) + return count, nil + })) + + executor := newEntityExecutor(r) + entityCtx := context.Background() + + // Batch 1: increment 3 times + req := &protos.EntityBatchRequest{ + InstanceId: "@counter@persist", + Operations: []*protos.OperationRequest{ + {Operation: "increment", RequestId: "r1"}, + {Operation: "increment", RequestId: "r2"}, + {Operation: "increment", RequestId: "r3"}, + }, + } + result, err := executor.ExecuteEntity(entityCtx, req) + require.NoError(t, err) + require.Len(t, result.Results, 3) + assert.Equal(t, "3", result.EntityState.GetValue()) + + // Batch 2: use state from batch 1, increment 2 more times + req2 := &protos.EntityBatchRequest{ + InstanceId: "@counter@persist", + EntityState: result.EntityState, + Operations: []*protos.OperationRequest{ + {Operation: "increment", RequestId: "r4"}, + {Operation: "get", RequestId: "r5"}, + }, + } + result2, err := executor.ExecuteEntity(entityCtx, req2) + require.NoError(t, err) + require.Len(t, result2.Results, 2) + // After 4th increment: 4 + assert.Equal(t, "4", result2.Results[0].GetSuccess().GetResult().GetValue()) + // Get returns 4 + assert.Equal(t, "4", result2.Results[1].GetSuccess().GetResult().GetValue()) + assert.Equal(t, "4", result2.EntityState.GetValue()) +} + +// When an operation fails, state rolls back to the last successful commit +func Test_Executor_EntityErrorRollbackInBatch(t *testing.T) { + r := task.NewTaskRegistry() + require.NoError(t, r.AddEntityN("rollback", func(ctx *task.EntityContext) (any, error) { + var val int + if ctx.HasState() { + _ = ctx.GetState(&val) + } + switch ctx.Operation { + case "set": + var newVal int + if err := ctx.GetInput(&newVal); err != nil { + return nil, err + } + val = newVal + _ = ctx.SetState(val) + return val, nil + case "fail_after_set": + val = 999 // modify state... + _ = ctx.SetState(val) + return nil, fmt.Errorf("intentional failure") // ...then fail + case "get": + _ = ctx.SetState(val) + return val, nil + } + return nil, fmt.Errorf("unknown op") + })) + + executor := newEntityExecutor(r) + entityCtx := context.Background() + + req := &protos.EntityBatchRequest{ + InstanceId: "@rollback@key1", + Operations: []*protos.OperationRequest{ + {Operation: "set", RequestId: "r1", Input: wrapperspb.String("10")}, + {Operation: "fail_after_set", RequestId: "r2"}, // fails, state should rollback + {Operation: "get", RequestId: "r3"}, + }, + } + + result, err := executor.ExecuteEntity(entityCtx, req) + require.NoError(t, err) + require.Len(t, result.Results, 3) + + // First op succeeds with value 10 + require.NotNil(t, result.Results[0].GetSuccess()) + assert.Equal(t, "10", result.Results[0].GetSuccess().GetResult().GetValue()) + + // Second op fails + require.NotNil(t, result.Results[1].GetFailure()) + assert.Contains(t, result.Results[1].GetFailure().GetFailureDetails().GetErrorMessage(), "intentional failure") + + // Third op sees state 10 (rolled back from 999) + require.NotNil(t, result.Results[2].GetSuccess()) + assert.Equal(t, "10", result.Results[2].GetSuccess().GetResult().GetValue()) + + // Final state is 10 (not 999) + assert.Equal(t, "10", result.EntityState.GetValue()) +} + +func Test_Executor_EntityWildcardRegistration(t *testing.T) { + r := task.NewTaskRegistry() + // Register a wildcard entity that handles any entity name + require.NoError(t, r.AddEntityN("*", func(ctx *task.EntityContext) (any, error) { + return fmt.Sprintf("handled %s on %s", ctx.Operation, ctx.ID.Name), nil + })) + + executor := newEntityExecutor(r) + entityCtx := context.Background() + + req := &protos.EntityBatchRequest{ + InstanceId: "@anything@key1", + Operations: []*protos.OperationRequest{ + {Operation: "test", RequestId: "r1"}, + }, + } + + result, err := executor.ExecuteEntity(entityCtx, req) + require.NoError(t, err) + require.Len(t, result.Results, 1) + require.NotNil(t, result.Results[0].GetSuccess()) + assert.Equal(t, `"handled test on anything"`, result.Results[0].GetSuccess().GetResult().GetValue()) +} + +func Test_Executor_EntitySignalAndStartOrchestrationActions(t *testing.T) { + r := task.NewTaskRegistry() + require.NoError(t, r.AddEntityN("coordinator", func(ctx *task.EntityContext) (any, error) { + if ctx.Operation == "notify_all" { + // Signal another entity + _ = ctx.SignalEntity(api.NewEntityID("worker", "w1"), "process", nil) + _ = ctx.SignalEntity(api.NewEntityID("worker", "w2"), "process", nil) + // Start an orchestration + _ = ctx.StartNewOrchestration("CleanupOrchestrator", + task.WithEntityStartOrchestrationInstanceID("cleanup-1"), + task.WithEntityStartOrchestrationInput("batch-42"), + ) + return "notified", nil + } + return nil, nil + })) + + executor := newEntityExecutor(r) + entityCtx := context.Background() + + req := &protos.EntityBatchRequest{ + InstanceId: "@coordinator@main", + Operations: []*protos.OperationRequest{ + {Operation: "notify_all", RequestId: "r1"}, + }, + } + + result, err := executor.ExecuteEntity(entityCtx, req) + require.NoError(t, err) + require.Len(t, result.Results, 1) + require.NotNil(t, result.Results[0].GetSuccess()) + + // Should have 3 actions: 2 signals + 1 start orchestration + require.Len(t, result.Actions, 3) + + signal1 := result.Actions[0].GetSendSignal() + require.NotNil(t, signal1) + assert.Equal(t, "@worker@w1", signal1.InstanceId) + assert.Equal(t, "process", signal1.Name) + + signal2 := result.Actions[1].GetSendSignal() + require.NotNil(t, signal2) + assert.Equal(t, "@worker@w2", signal2.InstanceId) + + startOrch := result.Actions[2].GetStartNewOrchestration() + require.NotNil(t, startOrch) + assert.Equal(t, "CleanupOrchestrator", startOrch.Name) + assert.Equal(t, "cleanup-1", startOrch.InstanceId) + assert.Equal(t, `"batch-42"`, startOrch.Input.GetValue()) +} + +func Test_Executor_EntityComplexState(t *testing.T) { + type item struct { + Name string `json:"name"` + Price int `json:"price"` + } + type cart struct { + Items []item `json:"items"` + Total int `json:"total"` + } + + r := task.NewTaskRegistry() + require.NoError(t, r.AddEntityN("cart", func(ctx *task.EntityContext) (any, error) { + var state cart + if ctx.HasState() { + _ = ctx.GetState(&state) + } + switch ctx.Operation { + case "add_item": + var i item + if err := ctx.GetInput(&i); err != nil { + return nil, err + } + state.Items = append(state.Items, i) + state.Total += i.Price + _ = ctx.SetState(state) + return len(state.Items), nil + case "get": + _ = ctx.SetState(state) + return state, nil + case "clear": + _ = ctx.SetState(nil) + return nil, nil + } + return nil, nil + })) + + executor := newEntityExecutor(r) + entityCtx := context.Background() + + req := &protos.EntityBatchRequest{ + InstanceId: "@cart@user1", + Operations: []*protos.OperationRequest{ + {Operation: "add_item", RequestId: "r1", Input: wrapperspb.String(`{"name":"apple","price":3}`)}, + {Operation: "add_item", RequestId: "r2", Input: wrapperspb.String(`{"name":"banana","price":2}`)}, + {Operation: "get", RequestId: "r3"}, + }, + } + + result, err := executor.ExecuteEntity(entityCtx, req) + require.NoError(t, err) + require.Len(t, result.Results, 3) + + // 1 item after first add + assert.Equal(t, "1", result.Results[0].GetSuccess().GetResult().GetValue()) + // 2 items after second add + assert.Equal(t, "2", result.Results[1].GetSuccess().GetResult().GetValue()) + // Get returns full cart + getResult := result.Results[2].GetSuccess().GetResult().GetValue() + assert.Contains(t, getResult, `"apple"`) + assert.Contains(t, getResult, `"banana"`) + assert.Contains(t, getResult, `"total":5`) +} + +func Test_Executor_EntityEmptyBatch(t *testing.T) { + r := task.NewTaskRegistry() + require.NoError(t, r.AddEntityN("noop", func(ctx *task.EntityContext) (any, error) { + return nil, nil + })) + + executor := newEntityExecutor(r) + entityCtx := context.Background() + + req := &protos.EntityBatchRequest{ + InstanceId: "@noop@key1", + Operations: []*protos.OperationRequest{}, + } + + result, err := executor.ExecuteEntity(entityCtx, req) + require.NoError(t, err) + assert.Empty(t, result.Results) + assert.Empty(t, result.Actions) +} diff --git a/tests/entity_integration_test.go b/tests/entity_integration_test.go new file mode 100644 index 00000000..58f1d5c5 --- /dev/null +++ b/tests/entity_integration_test.go @@ -0,0 +1,230 @@ +// Integration tests for in-process entity execution via the orchestration worker. +package tests + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/microsoft/durabletask-go/api" + "github.com/microsoft/durabletask-go/backend" + "github.com/microsoft/durabletask-go/task" +) + +// Test that an entity can be signaled from the client and processed in-process. +func Test_InProcess_Entity_SignalAndQuery(t *testing.T) { + r := task.NewTaskRegistry() + require.NoError(t, r.AddEntityN("counter", func(ctx *task.EntityContext) (any, error) { + var count int + if ctx.HasState() { + _ = ctx.GetState(&count) + } + switch ctx.Operation { + case "add": + var amount int + if err := ctx.GetInput(&amount); err != nil { + return nil, err + } + count += amount + case "get": + // no-op, just return + } + _ = ctx.SetState(count) + return count, nil + })) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + baseClient, worker := initTaskHubWorker(ctx, r) + client := baseClient.(backend.EntityTaskHubClient) + defer func() { + if err := worker.Shutdown(context.Background()); err != nil { + t.Logf("shutdown: %v", err) + } + }() + + entityID := api.NewEntityID("counter", "test1") + + // Signal the entity to add 5 + err := client.SignalEntity(ctx, entityID, "add", api.WithSignalInput(5)) + require.NoError(t, err) + + // Signal again to add 3 + err = client.SignalEntity(ctx, entityID, "add", api.WithSignalInput(3)) + require.NoError(t, err) + + // Poll until state contains "8" + require.Eventually(t, func() bool { + meta, err := client.FetchEntityMetadata(ctx, entityID, true) + if err != nil || meta == nil { + return false + } + return assert.ObjectsAreEqual(entityID, meta.InstanceID) && + strings.Contains(meta.SerializedState, "8") + }, 10*time.Second, 200*time.Millisecond) +} + +func Test_InProcess_Entity_SignalScheduledTime(t *testing.T) { + r := task.NewTaskRegistry() + require.NoError(t, r.AddEntityN("counter", func(ctx *task.EntityContext) (any, error) { + var count int + if ctx.HasState() { + _ = ctx.GetState(&count) + } + if ctx.Operation == "add" { + var amount int + if err := ctx.GetInput(&amount); err != nil { + return nil, err + } + count += amount + } + _ = ctx.SetState(count) + return count, nil + })) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + baseClient, worker := initTaskHubWorker(ctx, r) + client := baseClient.(backend.EntityTaskHubClient) + defer func() { + if err := worker.Shutdown(context.Background()); err != nil { + t.Logf("shutdown: %v", err) + } + }() + + entityID := api.NewEntityID("counter", "scheduled") + fireAt := time.Now().Add(750 * time.Millisecond) + + require.NoError(t, client.SignalEntity(ctx, entityID, "add", api.WithSignalInput(5), api.WithSignalScheduledTime(fireAt))) + + require.Never(t, func() bool { + meta, err := client.FetchEntityMetadata(ctx, entityID, true) + if err != nil || meta == nil { + return false + } + return strings.Contains(meta.SerializedState, "5") + }, 300*time.Millisecond, 100*time.Millisecond) + + require.Eventually(t, func() bool { + meta, err := client.FetchEntityMetadata(ctx, entityID, true) + if err != nil || meta == nil { + return false + } + return strings.Contains(meta.SerializedState, "5") + }, 10*time.Second, 100*time.Millisecond) +} + +// Test that entities work with the auto-dispatch pattern. +func Test_InProcess_Entity_AutoDispatch(t *testing.T) { + r := task.NewTaskRegistry() + require.NoError(t, r.AddEntityN("counter", func(ctx *task.EntityContext) (any, error) { + var val int + if ctx.HasState() { + _ = ctx.GetState(&val) + } + switch ctx.Operation { + case "increment": + val++ + case "decrement": + val-- + case "get": + } + _ = ctx.SetState(val) + return val, nil + })) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + baseClient, worker := initTaskHubWorker(ctx, r) + client := baseClient.(backend.EntityTaskHubClient) + defer func() { + if err := worker.Shutdown(context.Background()); err != nil { + t.Logf("shutdown: %v", err) + } + }() + + entityID := api.NewEntityID("counter", "auto") + + // Send multiple signals + require.NoError(t, client.SignalEntity(ctx, entityID, "increment")) + require.NoError(t, client.SignalEntity(ctx, entityID, "increment")) + require.NoError(t, client.SignalEntity(ctx, entityID, "increment")) + require.NoError(t, client.SignalEntity(ctx, entityID, "decrement")) + + // Poll until state contains "2" + require.Eventually(t, func() bool { + meta, err := client.FetchEntityMetadata(ctx, entityID, true) + if err != nil || meta == nil { + return false + } + return strings.Contains(meta.SerializedState, "2") + }, 10*time.Second, 200*time.Millisecond) +} + +// Test that CallEntity works end-to-end: an orchestration calls an entity and gets a response. +func Test_InProcess_Entity_CallEntity(t *testing.T) { + r := task.NewTaskRegistry() + + // Register a counter entity + require.NoError(t, r.AddEntityN("counter", func(ctx *task.EntityContext) (any, error) { + var count int + if ctx.HasState() { + _ = ctx.GetState(&count) + } + switch ctx.Operation { + case "add": + var amount int + if err := ctx.GetInput(&amount); err != nil { + return nil, err + } + count += amount + case "get": + // just return + } + _ = ctx.SetState(count) + return count, nil + })) + + // Register an orchestration that calls the entity and returns the result + require.NoError(t, r.AddOrchestratorN("CallEntityOrchestrator", func(ctx *task.OrchestrationContext) (any, error) { + entityID := api.NewEntityID("counter", "fromOrch") + + // Call entity (request-response) to add and get result + var result int + if err := ctx.CallEntity(entityID, "add", task.WithEntityInput(15)).Await(&result); err != nil { + return nil, err + } + + return result, nil + })) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + baseClient, worker := initTaskHubWorker(ctx, r) + defer func() { + if err := worker.Shutdown(context.Background()); err != nil { + t.Logf("shutdown: %v", err) + } + }() + + // Note: we do NOT pre-create the entity — the orchestration processor + // auto-creates entity instances when pending messages target entity IDs. + + // Run the orchestration + id, err := baseClient.ScheduleNewOrchestration(ctx, "CallEntityOrchestrator") + require.NoError(t, err) + + // Wait for orchestration to complete + metadata, err := baseClient.WaitForOrchestrationCompletion(ctx, id) + require.NoError(t, err) + assert.Equal(t, "ORCHESTRATION_STATUS_COMPLETED", metadata.RuntimeStatus.String()) + assert.Contains(t, metadata.SerializedOutput, "15") +} diff --git a/tests/mocks/EntityBackend.go b/tests/mocks/EntityBackend.go new file mode 100644 index 00000000..3fafbd15 --- /dev/null +++ b/tests/mocks/EntityBackend.go @@ -0,0 +1,144 @@ +package mocks + +import ( + "context" + + "github.com/microsoft/durabletask-go/api" + "github.com/stretchr/testify/mock" +) + +// EntityBackend is a mock that implements both backend.Backend and backend.EntityBackend. +type EntityBackend struct { + Backend +} + +type EntityBackend_Expecter struct { + mock *mock.Mock +} + +func (_m *EntityBackend) EXPECT() *EntityBackend_Expecter { + return &EntityBackend_Expecter{mock: &_m.Mock} +} + +// GetEntityMetadata provides a mock function +func (_m *EntityBackend) GetEntityMetadata(_a0 context.Context, _a1 api.EntityID, _a2 bool) (*api.EntityMetadata, error) { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for GetEntityMetadata") + } + + var r0 *api.EntityMetadata + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, api.EntityID, bool) (*api.EntityMetadata, error)); ok { + return rf(_a0, _a1, _a2) + } + if rf, ok := ret.Get(0).(func(context.Context, api.EntityID, bool) *api.EntityMetadata); ok { + r0 = rf(_a0, _a1, _a2) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.EntityMetadata) + } + } + if rf, ok := ret.Get(1).(func(context.Context, api.EntityID, bool) error); ok { + r1 = rf(_a0, _a1, _a2) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +func (_e *EntityBackend_Expecter) GetEntityMetadata(_a0 interface{}, _a1 interface{}, _a2 interface{}) *EntityBackend_GetEntityMetadata_Call { + return &EntityBackend_GetEntityMetadata_Call{Call: _e.mock.On("GetEntityMetadata", _a0, _a1, _a2)} +} + +type EntityBackend_GetEntityMetadata_Call struct { + *mock.Call +} + +func (_c *EntityBackend_GetEntityMetadata_Call) Return(_a0 *api.EntityMetadata, _a1 error) *EntityBackend_GetEntityMetadata_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +// QueryEntities provides a mock function +func (_m *EntityBackend) QueryEntities(_a0 context.Context, _a1 api.EntityQuery) (*api.EntityQueryResults, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for QueryEntities") + } + + var r0 *api.EntityQueryResults + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, api.EntityQuery) (*api.EntityQueryResults, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, api.EntityQuery) *api.EntityQueryResults); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.EntityQueryResults) + } + } + if rf, ok := ret.Get(1).(func(context.Context, api.EntityQuery) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +func (_e *EntityBackend_Expecter) QueryEntities(_a0 interface{}, _a1 interface{}) *EntityBackend_QueryEntities_Call { + return &EntityBackend_QueryEntities_Call{Call: _e.mock.On("QueryEntities", _a0, _a1)} +} + +type EntityBackend_QueryEntities_Call struct { + *mock.Call +} + +func (_c *EntityBackend_QueryEntities_Call) Return(_a0 *api.EntityQueryResults, _a1 error) *EntityBackend_QueryEntities_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +// CleanEntityStorage provides a mock function +func (_m *EntityBackend) CleanEntityStorage(_a0 context.Context, _a1 api.CleanEntityStorageRequest) (*api.CleanEntityStorageResult, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for CleanEntityStorage") + } + + var r0 *api.CleanEntityStorageResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, api.CleanEntityStorageRequest) (*api.CleanEntityStorageResult, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, api.CleanEntityStorageRequest) *api.CleanEntityStorageResult); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.CleanEntityStorageResult) + } + } + if rf, ok := ret.Get(1).(func(context.Context, api.CleanEntityStorageRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +func (_e *EntityBackend_Expecter) CleanEntityStorage(_a0 interface{}, _a1 interface{}) *EntityBackend_CleanEntityStorage_Call { + return &EntityBackend_CleanEntityStorage_Call{Call: _e.mock.On("CleanEntityStorage", _a0, _a1)} +} + +type EntityBackend_CleanEntityStorage_Call struct { + *mock.Call +} + +func (_c *EntityBackend_CleanEntityStorage_Call) Return(_a0 *api.CleanEntityStorageResult, _a1 error) *EntityBackend_CleanEntityStorage_Call { + _c.Call.Return(_a0, _a1) + return _c +}