Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add signalEntity support to v3 #464

Merged
merged 2 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add signalEntity API to orchestrations (#383)
* scaffolding

* make fire-and-forget action non yieldable

* initial unit tests

* add some unit tests

* update tests

* remove unecessary reference

* update error message
  • Loading branch information
hossam-nasr committed Feb 3, 2023
commit 31fcc0dad156767216b2b3d91746203d0a4223d2
4 changes: 2 additions & 2 deletions src/actions/actiontype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export enum ActionType {
WaitForExternalEvent = 6,
CallEntity = 7,
CallHttp = 8,
// ActionType 9 and 10 correspond to SignalEntity and ScheduledSignalEntity
// Those two are not supported yet.
SignalEntity = 9,
// ActionType 10 corresponds to ScheduledSignalEntity, which is not supported yet
WhenAny = 11,
WhenAll = 12,
}
17 changes: 17 additions & 0 deletions src/actions/signalentityaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ActionType, EntityId, IAction, Utils } from "../classes";

/** @hidden */
export class SignalEntityAction implements IAction {
public readonly actionType: ActionType = ActionType.SignalEntity;
public readonly instanceId: string;
public readonly input: unknown;

constructor(entityId: EntityId, public readonly operation: string, input?: unknown) {
if (!entityId) {
throw new Error("Must provide EntityId to SignalEntityAction constructor");
}
this.input = Utils.processInput(input);
Utils.throwIfEmpty(operation, "operation");
this.instanceId = EntityId.getSchedulerIdFromEntityId(entityId);
}
}
14 changes: 14 additions & 0 deletions src/durableorchestrationcontext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
import moment = require("moment");
import { ReplaySchema } from "./replaySchema";
import { CallHttpOptions, Task, TimerTask } from "./types";
import { SignalEntityAction } from "./actions/signalentityaction";

/**
* Parameter data for orchestration bindings that can be used to schedule
Expand Down Expand Up @@ -239,6 +240,19 @@ export class DurableOrchestrationContext {
return task;
}

/**
* Send a signal operation to a Durable Entity, passing an argument, without
* waiting for a response. A fire-and-forget operation.
*
* @param entityId ID of the target entity.
* @param operationName The name of the operation.
* @param operationInput (optional) input for the operation.
*/
public signalEntity(entityId: EntityId, operationName: string, operationInput?: unknown): void {
const action = new SignalEntityAction(entityId, operationName, operationInput);
this.taskOrchestratorExecutor.recordFireAndForgetAction(action);
}

/**
* Schedules an orchestration function named `name` for execution.
*
Expand Down
22 changes: 18 additions & 4 deletions src/taskorchestrationexecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,10 +390,17 @@ export class TaskOrchestrationExecutor {
newTask = generatorResult.value;
} else {
// non-task was yielded. This isn't supported
throw Error(
`Orchestration yielded data of type ${typeof generatorResult.value}. Only Task types can be yielded.` +
"Please refactor your orchestration to yield only Tasks."
);
let errorMsg = `Durable Functions programming constraint violation: Orchestration yielded data of type ${typeof generatorResult.value}.`;
errorMsg +=
typeof generatorResult.value === "undefined"
? ' This is likely a result of yielding a "fire-and-forget API" such as signalEntity or continueAsNew.' +
" These APIs should not be yielded as they are not blocking operations. Please remove the yield statement preceding those invocations." +
" If you are not calling those APIs, p"
: " Only Task types can be yielded. P";
errorMsg +=
"lease check your yield statements to make sure you only yield Task types resulting from calling Durable Functions APIs.";

throw Error(errorMsg);
}
} catch (exception) {
// The generator threw an exception
Expand Down Expand Up @@ -433,6 +440,13 @@ export class TaskOrchestrationExecutor {
}
}

public recordFireAndForgetAction(action: IAction): void {
if (!this.willContinueAsNew) {
this.addToActions(action);
this.sequenceNumber++;
}
}

/**
* @hidden
* Tracks this task as waiting for completion.
Expand Down
130 changes: 130 additions & 0 deletions test/integration/orchestrator-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "mocha";
import * as moment from "moment";
import * as uuidv1 from "uuid/v1";
import { DummyOrchestrationContext, ManagedIdentityTokenSource } from "../../src";
import { SignalEntityAction } from "../../src/actions/signalentityaction";
import {
ActionType,
CallActivityAction,
Expand Down Expand Up @@ -56,6 +57,38 @@ describe("Orchestrator", () => {
);
});

it("doesn't allow yielding non-Task types", async () => {
const orchestrator = TestOrchestrations.YieldInteger;
const mockContext = new MockContext({
context: new DurableOrchestrationBindingInfo(
TestHistories.StarterHistory(moment.utc().toDate())
),
});

orchestrator(mockContext);

const errorMsg =
`Durable Functions programming constraint violation: Orchestration yielded data of type number.` +
" Only Task types can be yielded. Please check your yield statements to make sure you only yield Task types resulting from calling Durable Functions APIs.";

const expectedErr = new OrchestrationFailureError(
false,
new OrchestratorState(
{
isDone: false,
actions: [],
schemaVersion: ReplaySchema.V1,
error: errorMsg,
output: undefined,
},
true
)
);

expect(mockContext.doneValue).to.be.undefined;
expect(mockContext.err?.toString()).to.equal(expectedErr.toString());
});

it("handles a simple orchestration function (no activity functions)", async () => {
const orchestrator = TestOrchestrations.SayHelloInline;
const name = "World";
Expand Down Expand Up @@ -1294,6 +1327,103 @@ describe("Orchestrator", () => {
});
});

describe("signalEntity()", () => {
it("scheduled a SignalEntity action", () => {
const orchestrator = TestOrchestrations.signalEntity;
const entityName = "Counter";
const id = "1234";
const expectedEntity = new EntityId(entityName, id);
const operationName = "add";
const operationArgument = 1;
const mockContext = new MockContext({
context: new DurableOrchestrationBindingInfo(
TestHistories.GetOrchestratorStart("signalEntity", new Date()),
{
id,
entityName,
operationName,
operationArgument,
}
),
});

orchestrator(mockContext);

expect(mockContext.doneValue).to.deep.equal(
new OrchestratorState(
{
isDone: true,
output: undefined,
actions: [
[
new SignalEntityAction(
expectedEntity,
operationName,
operationArgument
),
],
],
schemaVersion: ReplaySchema.V1,
},
true
)
);
});

it("doesn't allow signalEntity() to be yielded", () => {
const orchestrator = TestOrchestrations.signalEntityYield;
const entityName = "Counter";
const id = "1234";
const expectedEntity = new EntityId(entityName, id);
const operationName = "add";
const operationArgument = 1;
const mockContext = new MockContext({
context: new DurableOrchestrationBindingInfo(
TestHistories.GetOrchestratorStart("signalEntity", new Date()),
{
id,
entityName,
operationName,
operationArgument,
}
),
});

orchestrator(mockContext);

const errorMsg =
`Durable Functions programming constraint violation: Orchestration yielded data of type undefined.` +
' This is likely a result of yielding a "fire-and-forget API" such as signalEntity or continueAsNew.' +
" These APIs should not be yielded as they are not blocking operations. Please remove the yield statement preceding those invocations." +
" If you are not calling those APIs, please check your yield statements to make sure you only yield Task types resulting from calling Durable Functions APIs.";

const expectedErr = new OrchestrationFailureError(
false,
new OrchestratorState(
{
isDone: false,
actions: [
[
new SignalEntityAction(
expectedEntity,
operationName,
operationArgument
),
],
],
schemaVersion: ReplaySchema.V1,
error: errorMsg,
output: undefined,
},
true
)
);

expect(mockContext.doneValue).to.be.undefined;
expect(mockContext.err?.toString()).to.equal(expectedErr.toString());
});
});

describe("callSubOrchestrator()", () => {
it("schedules a suborchestrator function", async () => {
const orchestrator = TestOrchestrations.SayHelloWithSubOrchestrator;
Expand Down
18 changes: 18 additions & 0 deletions test/testobjects/TestOrchestrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export class TestOrchestrations {
return "Hello";
});

public static YieldInteger: any = createOrchestrator(function* () {
yield 4;
});

public static AnyAOrB: any = createOrchestrator(function* (context: any) {
const completeInOrder = context.df.getInput();

Expand Down Expand Up @@ -104,6 +108,20 @@ export class TestOrchestrations {
return currentValue;
});

public static signalEntity: any = createOrchestrator(function* (context: any) {
const { id, entityName, operationName, operationArgument } = context.df.getInput();
const entityId = new df.EntityId(entityName, id);
context.df.signalEntity(entityId, operationName, operationArgument);
return;
});

public static signalEntityYield: any = createOrchestrator(function* (context: any) {
const { id, entityName, operationName, operationArgument } = context.df.getInput();
const entityId = new df.EntityId(entityName, id);
yield context.df.signalEntity(entityId, operationName, operationArgument);
return;
});

public static FanOutFanInDiskUsage: any = createOrchestrator(function* (context: any) {
const directory = context.df.getInput();
const files = yield context.df.callActivity("GetFileList", directory);
Expand Down