EdgeAIG's picture
download
raw
22 kB
/**
* The cluster workflow engine runs durable workflows on top of cluster sharding
* and message storage. It adapts `WorkflowEngine.WorkflowEngine` so workflow
* executions, activities, deferred completions, resumes, interrupts, and durable
* clock wakeups are represented as persisted cluster entity messages.
*
* @since 4.0.0
*/
import * as Context from "../../Context.js";
import * as DateTime from "../../DateTime.js";
import * as Duration from "../../Duration.js";
import * as Effect from "../../Effect.js";
import * as Exit from "../../Exit.js";
import * as Fiber from "../../Fiber.js";
import * as Latch from "../../Latch.js";
import * as Layer from "../../Layer.js";
import * as Option from "../../Option.js";
import * as PrimaryKey from "../../PrimaryKey.js";
import * as RcMap from "../../RcMap.js";
import * as Schedule from "../../Schedule.js";
import * as Schema from "../../Schema.js";
import * as Headers from "../http/Headers.js";
import * as Rpc from "../rpc/Rpc.js";
import { ClientAbort } from "../rpc/RpcSchema.js";
import * as Activity from "../workflow/Activity.js";
import * as DurableClock from "../workflow/DurableClock.js";
import * as DurableDeferred from "../workflow/DurableDeferred.js";
import * as Workflow from "../workflow/Workflow.js";
import * as WorkflowEngine from "../workflow/WorkflowEngine.js";
import * as ClusterSchema from "./ClusterSchema.js";
import * as DeliverAt from "./DeliverAt.js";
import * as Entity from "./Entity.js";
import * as EntityAddress from "./EntityAddress.js";
import * as EntityId from "./EntityId.js";
import * as EntityType from "./EntityType.js";
import * as Envelope from "./Envelope.js";
import * as Message from "./Message.js";
import { MessageStorage } from "./MessageStorage.js";
import * as Reply from "./Reply.js";
import * as Sharding from "./Sharding.js";
import * as Snowflake from "./Snowflake.js";
/**
* Creates a `WorkflowEngine` implementation backed by cluster sharding and
* message storage.
*
* **Details**
*
* Workflow executions, activities, deferred completions, resumes, interrupts,
* and durable clock wakeups are coordinated through persisted cluster entities.
*
* @category constructors
* @since 4.0.0
*/
export const make = /*#__PURE__*/Effect.gen(function* () {
const sharding = yield* Sharding.Sharding;
const storage = yield* MessageStorage;
const workflows = new Map();
const entities = new Map();
const partialEntities = new Map();
const ensureEntity = workflow => {
let entity = entities.get(workflow._tag);
if (!entity) {
entity = makeWorkflowEntity(workflow);
workflows.set(workflow._tag, workflow);
entities.set(workflow._tag, entity);
}
return entity;
};
const ensurePartialEntity = workflowName => {
let entity = partialEntities.get(workflowName);
if (!entity) {
entity = makePartialWorkflowEntity(workflowName);
partialEntities.set(workflowName, entity);
}
return entity;
};
const activities = new Map();
const interruptedActivities = new Set();
const activityLatches = new Map();
const clients = yield* RcMap.make({
lookup: Effect.fnUntraced(function* (workflowName) {
const entity = entities.get(workflowName);
if (!entity) {
return yield* Effect.die(`Workflow ${workflowName} not registered`);
}
return yield* entity.client;
}),
idleTimeToLive: "5 minutes"
});
const clientsPartial = yield* RcMap.make({
lookup: Effect.fnUntraced(function* (workflowName) {
const entity = entities.get(workflowName) ?? ensurePartialEntity(workflowName);
return yield* entity.client;
}),
idleTimeToLive: "5 minutes"
});
const entityAddressFor = options => {
const shardGroup = Context.get(options.workflow.annotations, ClusterSchema.ShardGroup)(options.executionId);
const entityId = EntityId.make(options.executionId);
return EntityAddress.make({
entityType: EntityType.make(options.entityType),
entityId,
shardId: sharding.getShardId(entityId, shardGroup)
});
};
const sendDiscard = Effect.fnUntraced(function* (options) {
const payload = options.rpc.payloadSchema.make(options.payload);
const envelope = Envelope.makeRequest({
requestId: yield* sharding.getSnowflake,
address: options.address,
tag: options.rpc._tag,
payload,
headers: Headers.empty
});
yield* sharding.sendOutgoing(new Message.OutgoingRequest({
envelope,
context: Context.empty(),
lastReceivedReply: Option.none(),
rpc: options.rpc,
respond: () => Effect.void,
annotations: Context.get(options.rpc.annotations, ClusterSchema.Dynamic)(options.rpc.annotations, envelope)
}), true);
});
const requestIdFor = Effect.fnUntraced(function* (options) {
const address = entityAddressFor(options);
return yield* storage.requestIdForPrimaryKey({
address,
tag: options.tag,
id: options.id
});
});
const replyForRequestId = Effect.fnUntraced(function* (requestId) {
const replies = yield* storage.repliesForUnfiltered([requestId]);
const last = replies[replies.length - 1];
if (last && last._tag === "WithExit") {
return Option.some(last);
}
return Option.none();
});
const requestReply = Effect.fnUntraced(function* (options) {
const requestId = yield* requestIdFor(options);
if (Option.isNone(requestId)) {
return Option.none();
}
return yield* replyForRequestId(requestId.value);
});
const resetActivityAttempt = Effect.fnUntraced(function* (options) {
const requestId = yield* requestIdFor({
workflow: options.workflow,
entityType: `Workflow/${options.workflow._tag}`,
executionId: options.executionId,
tag: "activity",
id: activityPrimaryKey(options.activity.name, options.attempt)
});
if (Option.isNone(requestId)) return;
yield* sharding.reset(requestId.value);
}, Effect.retry({
times: 3,
schedule: Schedule.exponential(250)
}), Effect.orDie);
const clearClock = Effect.fnUntraced(function* (options) {
const shardGroup = Context.get(options.workflow.annotations, ClusterSchema.ShardGroup)(options.executionId);
const entityId = EntityId.make(options.executionId);
const shardId = sharding.getShardId(entityId, shardGroup);
const clockAddress = EntityAddress.make({
entityType: ClockEntity.type,
entityId,
shardId
});
yield* storage.clearAddress(clockAddress);
});
const resume = Effect.fnUntraced(function* (workflow, executionId) {
const maybeReply = yield* requestReply({
workflow,
entityType: `Workflow/${workflow._tag}`,
executionId,
tag: "run",
id: ""
});
const maybeSuspended = Option.filter(maybeReply, reply => reply.exit._tag === "Success" && reply.exit.value._tag === "Suspended");
if (Option.isNone(maybeSuspended)) return;
yield* sharding.reset(Snowflake.Snowflake(maybeSuspended.value.requestId));
yield* sharding.pollStorage;
});
const sendResumeParent = Effect.fnUntraced(function* (options) {
const requestId = yield* requestIdFor({
workflow: workflows.get(options.workflowName),
entityType: `Workflow/${options.workflowName}`,
executionId: options.executionId,
tag: "resume",
id: ""
});
if (Option.isNone(requestId)) {
const client = (yield* RcMap.get(clientsPartial, options.workflowName))(options.executionId);
return yield* client.resume({}, {
discard: true
});
}
const reply = yield* replyForRequestId(requestId.value);
if (Option.isNone(reply)) return;
yield* sharding.reset(requestId.value);
}, Effect.scoped);
const interrupt = Effect.fnUntraced(function* (workflow, executionId) {
ensureEntity(workflow);
const requestId = yield* requestIdFor({
workflow,
entityType: `Workflow/${workflow._tag}`,
executionId,
tag: "run",
id: ""
});
if (Option.isNone(requestId)) {
return Option.none();
}
const reply = yield* replyForRequestId(requestId.value);
const nonSuspendedReply = Option.filter(reply, reply => reply.exit._tag !== "Success" || reply.exit.value._tag !== "Suspended");
if (Option.isSome(nonSuspendedReply)) {
return Option.none();
}
yield* engine.deferredDone(InterruptSignal, {
workflowName: workflow._tag,
executionId,
deferredName: InterruptSignal.name,
exit: Exit.void
});
return requestId;
}, Effect.retry({
while: e => e._tag === "PersistenceError",
times: 3,
schedule: Schedule.exponential(250)
}), Effect.orDie);
const engine = WorkflowEngine.makeUnsafe({
register: (workflow, execute) => Effect.suspend(() => sharding.registerEntity(ensureEntity(workflow), Effect.gen(function* () {
const address = yield* Entity.CurrentAddress;
const executionId = address.entityId;
return {
run: request => {
const instance = WorkflowEngine.WorkflowInstance.initial(workflow, executionId);
const payload = request.payload;
let parent;
if (payload[payloadParentKey]) {
parent = payload[payloadParentKey];
}
return execute(workflow.payloadSchema.make(payload), executionId).pipe(Effect.onExit(exit => {
const suspendOnFailure = Context.get(workflow.annotations, Workflow.SuspendOnFailure);
if (!instance.suspended && !(suspendOnFailure && exit._tag === "Failure")) {
return parent ? ensureSuccess(sendResumeParent(parent)) : Effect.void;
}
return engine.deferredResult(InterruptSignal).pipe(Effect.flatMap(maybeExit => {
if (Option.isNone(maybeExit)) {
return Effect.void;
}
instance.suspended = false;
instance.interrupted = true;
return Effect.andThen(Effect.ignore(clearClock({
workflow,
executionId
})), Effect.withFiber(fiber => Effect.interruptible(Fiber.interrupt(fiber))));
}), Effect.orDie);
}), Workflow.intoResult, Effect.provideService(WorkflowEngine.WorkflowInstance, instance));
},
activity(request) {
const payload = request.payload;
const activityId = `${executionId}/${payload.name}`;
const instance = WorkflowEngine.WorkflowInstance.initial(workflow, executionId);
interruptedActivities.delete(activityId);
return Effect.gen(function* () {
let entry = activities.get(activityId);
while (!entry) {
const latch = Latch.makeUnsafe();
activityLatches.set(activityId, latch);
yield* latch.await;
entry = activities.get(activityId);
}
const contextMap = new Map(entry.context.mapUnsafe);
contextMap.set(Activity.CurrentAttempt.key, payload.attempt);
contextMap.set(WorkflowEngine.WorkflowInstance.key, instance);
return yield* entry.activity.executeEncoded.pipe(Effect.provideContext(Context.makeUnsafe(contextMap)));
}).pipe(Workflow.intoResult, Effect.catchCause(cause => {
// we only want to store interrupts as suspends when the
// client requested it
const suspend = cause.reasons.some(f => f._tag === "Interrupt" && f.annotations.has(ClientAbort.key));
if (suspend) {
interruptedActivities.add(activityId);
return Effect.succeed(new Workflow.Suspended({}));
}
return Effect.failCause(cause);
}), Effect.provideService(WorkflowEngine.WorkflowInstance, instance), Effect.provideService(Activity.CurrentAttempt, payload.attempt), Effect.ensuring(Effect.sync(() => {
activities.delete(activityId);
})), Rpc.wrap({
fork: true,
uninterruptible: true
}));
},
deferred: Effect.fnUntraced(function* (request) {
const payload = request.payload;
yield* ensureSuccess(resume(workflow, executionId));
return payload.exit;
}),
resume: () => ensureSuccess(resume(workflow, executionId))
};
}))),
execute: (workflow, {
discard,
executionId,
parent,
payload
}) => {
ensureEntity(workflow);
return RcMap.get(clients, workflow._tag).pipe(Effect.flatMap(make => make(executionId).run(parent ? {
...payload,
[payloadParentKey]: {
workflowName: parent.workflow._tag,
executionId: parent.executionId
}
} : payload, {
discard
})), Effect.orDie, Effect.scoped);
},
poll: Effect.fnUntraced(function* (workflow, executionId) {
const entity = ensureEntity(workflow);
const exitSchema = Schema.toCodecJson(Rpc.exitSchema(entity.protocol.requests.get("run")));
const reply = yield* requestReply({
workflow,
entityType: `Workflow/${workflow._tag}`,
executionId,
tag: "run",
id: ""
});
if (Option.isNone(reply)) return Option.none();
const exit = yield* Schema.decodeUnknownEffect(exitSchema)(reply.value.exit);
return Option.some(yield* exit);
}, Effect.orDie),
interrupt: (workflow, executionId) => Effect.asVoid(interrupt(workflow, executionId)),
interruptUnsafe: Effect.fnUntraced(function* (workflow, executionId) {
const requestId = yield* interrupt(workflow, executionId);
if (Option.isNone(requestId)) return;
const entity = ensureEntity(workflow);
const runRpc = entity.protocol.requests.get("run");
yield* Effect.orDie(sharding.sendOutgoing(new Message.OutgoingEnvelope({
rpc: runRpc,
envelope: new Envelope.Interrupt({
id: yield* sharding.getSnowflake,
address: entityAddressFor({
workflow,
entityType: `Workflow/${workflow._tag}`,
executionId
}),
requestId: requestId.value
})
}), false));
}),
resume: (workflow, executionId) => ensureSuccess(resume(workflow, executionId)),
activityExecute: Effect.fnUntraced(function* (activity, attempt) {
const services = yield* Effect.context();
const instance = Context.get(services, WorkflowEngine.WorkflowInstance);
yield* Effect.annotateCurrentSpan("executionId", instance.executionId);
const activityId = `${instance.executionId}/${activity.name}`;
const client = (yield* RcMap.get(clientsPartial, instance.workflow._tag))(instance.executionId);
while (true) {
if (!activities.has(activityId)) {
activities.set(activityId, {
activity,
context: services
});
const latch = activityLatches.get(activityId);
if (latch) {
yield* latch.open;
activityLatches.delete(activityId);
}
}
const result = yield* Effect.orDie(client.activity({
name: activity.name,
attempt,
withTransaction: Context.get(activity.annotations, ClusterSchema.WithTransaction)
}));
// If the activity has suspended and did not execute, we need to resume
// it by resetting the attempt and re-executing.
if (result._tag === "Suspended" && (activities.has(activityId) || interruptedActivities.has(activityId))) {
yield* resetActivityAttempt({
workflow: instance.workflow,
executionId: instance.executionId,
activity,
attempt
});
continue;
}
activities.delete(activityId);
return result;
}
}, Effect.scoped),
deferredResult: deferred => WorkflowEngine.WorkflowInstance.pipe(Effect.flatMap(instance => requestReply({
workflow: instance.workflow,
entityType: `Workflow/${instance.workflow._tag}`,
executionId: instance.executionId,
tag: "deferred",
id: deferred.name
})), Effect.map(reply => {
if (Option.isNone(reply)) {
return Option.none();
}
const decoded = decodeDeferredWithExit(reply.value);
return Option.some(decoded.exit._tag === "Success" ? decoded.exit.value : decoded.exit);
}), Effect.retry({
while: e => e._tag === "PersistenceError",
times: 3,
schedule: Schedule.exponential(250)
}), Effect.orDie),
deferredDone: Effect.fnUntraced(function* ({
deferredName,
executionId,
exit,
workflowName
}) {
const workflow = workflows.get(workflowName);
if (workflow) {
return yield* Effect.orDie(sendDiscard({
rpc: DeferredRpc,
address: entityAddressFor({
workflow,
entityType: `Workflow/${workflowName}`,
executionId
}),
payload: {
name: deferredName,
exit
}
}));
}
const client = yield* RcMap.get(clientsPartial, workflowName);
return yield* Effect.orDie(client(executionId).deferred({
name: deferredName,
exit
}, {
discard: true
}));
}, Effect.scoped),
scheduleClock(workflow, options) {
return DateTime.now.pipe(Effect.flatMap(now => sendDiscard({
rpc: ClockRpc,
address: entityAddressFor({
workflow,
entityType: ClockEntity.type,
executionId: options.executionId
}),
payload: {
name: options.clock.name,
workflowName: workflow._tag,
wakeUp: DateTime.addDuration(now, options.clock.duration)
}
})), Effect.orDie);
}
});
return engine;
});
const retryPolicy = /*#__PURE__*/Schedule.exponential(200, 1.5).pipe(/*#__PURE__*/Schedule.either(/*#__PURE__*/Schedule.spaced("1 minute")));
const ensureSuccess = effect => effect.pipe(Effect.sandbox, Effect.retry(retryPolicy), Effect.orDie);
const AnyOrVoid = /*#__PURE__*/Schema.Union([Schema.Undefined, Schema.Any]);
const ExitUnknown = /*#__PURE__*/Schema.Exit(AnyOrVoid, AnyOrVoid, Schema.Any);
const ActivityRpc = /*#__PURE__*/Rpc.make("activity", {
payload: {
name: Schema.String,
attempt: Schema.Number,
withTransaction: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false)))
},
primaryKey: ({
attempt,
name
}) => activityPrimaryKey(name, attempt),
success: Workflow.Result({
success: AnyOrVoid,
error: AnyOrVoid
})
}).annotate(ClusterSchema.Persisted, true).annotate(ClusterSchema.Dynamic, (annotations, request) => request.payload.withTransaction ? Context.add(annotations, ClusterSchema.WithTransaction, true) : annotations);
const DeferredRpc = /*#__PURE__*/Rpc.make("deferred", {
payload: {
name: Schema.String,
exit: ExitUnknown
},
primaryKey: ({
name
}) => name,
success: ExitUnknown
}).annotate(ClusterSchema.Persisted, true).annotate(ClusterSchema.Uninterruptible, true);
const decodeDeferredWithExit = /*#__PURE__*/Schema.decodeSync(/*#__PURE__*/Schema.toCodecJson(/*#__PURE__*/Reply.WithExit.schema(DeferredRpc)));
const ResumeRpc = /*#__PURE__*/Rpc.make("resume", {
payload: {},
primaryKey: () => ""
}).annotate(ClusterSchema.Persisted, true).annotate(ClusterSchema.Uninterruptible, "server");
const payloadParentKey = "~effect/cluster/ClusterWorkflowEngine/payloadParentKey";
const makeWorkflowEntity = workflow => Entity.make(`Workflow/${workflow._tag}`, [Rpc.make("run", {
payload: {
...workflow.payloadSchema.fields,
[payloadParentKey]: Schema.optional(Schema.Struct({
workflowName: Schema.String,
executionId: Schema.String
}))
},
primaryKey: () => "",
success: Workflow.Result({
success: workflow.successSchema,
error: workflow.errorSchema
})
}).annotate(ClusterSchema.Persisted, true).annotate(ClusterSchema.Uninterruptible, true), DeferredRpc, ResumeRpc, ActivityRpc]).annotateMerge(workflow.annotations);
const makePartialWorkflowEntity = workflowName => Entity.make(`Workflow/${workflowName}`, [DeferredRpc, ResumeRpc, ActivityRpc]);
const activityPrimaryKey = (activity, attempt) => `${activity}/${attempt}`;
class ClockPayload extends /*#__PURE__*/Schema.Class(`Workflow/DurableClock/Run`)({
name: Schema.String,
workflowName: Schema.String,
wakeUp: Schema.DateTimeUtcFromMillis
}) {
[PrimaryKey.symbol]() {
return this.name;
}
[DeliverAt.symbol]() {
return this.wakeUp;
}
}
const ClockRpc = /*#__PURE__*/Rpc.make("run", {
payload: ClockPayload
}).annotate(ClusterSchema.Persisted, true).annotate(ClusterSchema.Uninterruptible, true);
const ClockEntity = /*#__PURE__*/Entity.make("Workflow/-/DurableClock", [ClockRpc]);
const ClockEntityLayer = /*#__PURE__*/ClockEntity.toLayer(/*#__PURE__*/Effect.gen(function* () {
const engine = yield* WorkflowEngine.WorkflowEngine;
const address = yield* Entity.CurrentAddress;
const executionId = address.entityId;
return {
run(request) {
const deferred = DurableClock.make({
name: request.payload.name,
duration: Duration.zero
}).deferred;
return ensureSuccess(engine.deferredDone(deferred, {
workflowName: request.payload.workflowName,
executionId,
deferredName: deferred.name,
exit: Exit.void
}));
}
};
}));
const InterruptSignal = /*#__PURE__*/DurableDeferred.make("Workflow/InterruptSignal");
/**
* Layer that provides `WorkflowEngine.WorkflowEngine` using the cluster workflow
* engine implementation.
*
* **Details**
*
* It requires cluster sharding and message storage, and also registers the
* durable clock entity used for workflow wakeups.
*
* @category layers
* @since 4.0.0
*/
export const layer = /*#__PURE__*/ClockEntityLayer.pipe(/*#__PURE__*/Layer.provideMerge(/*#__PURE__*/Layer.effect(WorkflowEngine.WorkflowEngine)(make)));
//# sourceMappingURL=ClusterWorkflowEngine.js.map

Xet Storage Details

Size:
22 kB
·
Xet hash:
1b78e325c52ed42a9c2ea1551891e0f5ecdb6ac0bca401cbcc609d2364ff8dda

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.