Skip to content

Commit

Permalink
feat: report exported handlers for the workflow class to check for `r…
Browse files Browse the repository at this point in the history
…un` handler

We also want to validate if the exported workflows has the `run` handler
in the provided class entrypoint. I use the same approach as the
exported stateless classes: by introspecting the prototypes of the
workflow class.
  • Loading branch information
LuisDuarte1 committed Feb 21, 2025
1 parent 536b134 commit 30a5ba3
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 23 deletions.
46 changes: 27 additions & 19 deletions src/workerd/io/worker.c++
Original file line number Diff line number Diff line change
Expand Up @@ -2215,31 +2215,17 @@ void Worker::Lock::validateHandlers(ValidationErrorReporter& errorReporter) {
"did not produce a startup-time error.");
}
}
for (auto& entry: worker.impl->workflowClasses) {
KJ_IF_SOME(entrypointName, getEntrypointName(entry.key)) {
errorReporter.addWorkflowClass(entrypointName);
} else {
// Similiar to Durable Objects, Workflow cannot be the default entrypoint (at the time of writing).
LOG_PERIODICALLY(ERROR,
"Exported Workflow class cannot be the default entrypoint. This doesn't work, but historically "
"did not produce a startup-time error.");
}
}

for (auto& entry: worker.impl->statelessClasses) {
// We want to report all of the stateless class's members. To do this, we examine its
// prototype, and its prototype's prototype, and so on, until we get to Object's
// prototype, which we ignore.
auto entrypointName = getEntrypointName(entry.key);
auto getHandlersForClass = [&](EntrypointClass& entrypointClass) -> kj::Array<kj::String> {
kj::HashSet<kj::String> seenNames;
js.withinHandleScope([&]() {
// Find the prototype for `Object` by creating one.
auto obj = js.obj();
jsg::JsValue prototypeOfObject = obj.getPrototype(js);

// Walk the prototype chain.
jsg::JsObject ctor(KJ_ASSERT_NONNULL(entry.value.tryGetHandle(js.v8Isolate)));
jsg::JsObject ctor(KJ_ASSERT_NONNULL(entrypointClass.tryGetHandle(js.v8Isolate)));
jsg::JsValue proto = ctor.get(js, "prototype");
kj::HashSet<kj::String> seenNames;
for (;;) {
auto protoObj = JSG_REQUIRE_NONNULL(proto.tryCast<jsg::JsObject>(), TypeError,
"Exported entrypoint class's prototype chain does not end in Object.");
Expand All @@ -2266,9 +2252,31 @@ void Worker::Lock::validateHandlers(ValidationErrorReporter& errorReporter) {

proto = protoObj.getPrototype(js);
}

errorReporter.addEntrypoint(entrypointName, KJ_MAP(n, seenNames) { return kj::mv(n); });
});
return KJ_MAP(n, seenNames) { return kj::mv(n); };
};

for (auto& entry: worker.impl->workflowClasses) {
KJ_IF_SOME(entrypointName, getEntrypointName(entry.key)) {
// We also want to check for handlers in workflows - we primarily want to see if the provided worker
// has exposed the `run` handler inside of the class.
auto methods = getHandlersForClass(entry.value);
errorReporter.addWorkflowClass(entrypointName, kj::mv(methods));
} else {
// Similiar to Durable Objects, Workflow cannot be the default entrypoint (at the time of writing).
LOG_PERIODICALLY(ERROR,
"Exported Workflow class cannot be the default entrypoint. This doesn't work, but historically "
"did not produce a startup-time error.");
}
}

for (auto& entry: worker.impl->statelessClasses) {
// We want to report all of the stateless class's members. To do this, we examine its
// prototype, and its prototype's prototype, and so on, until we get to Object's
// prototype, which we ignore.
auto entrypointName = getEntrypointName(entry.key);
auto methods = getHandlersForClass(entry.value);
errorReporter.addEntrypoint(entrypointName, kj::mv(methods));
}
}
});
Expand Down
4 changes: 2 additions & 2 deletions src/workerd/io/worker.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class Worker: public kj::AtomicRefcounted {
virtual void addActorClass(kj::StringPtr exportName) = 0;

// Report that the Worker exports a Workflow class with the given name.
virtual void addWorkflowClass(kj::StringPtr exportName) = 0;
virtual void addWorkflowClass(kj::StringPtr exportName, kj::Array<kj::String> methods) = 0;
};

class LockType;
Expand Down Expand Up @@ -901,7 +901,7 @@ struct SimpleWorkerErrorReporter final: public Worker::ValidationErrorReporter {
KJ_UNREACHABLE;
}

void addWorkflowClass(kj::StringPtr exportName) override {
void addWorkflowClass(kj::StringPtr exportName, kj::Array<kj::String> methods) override {
KJ_UNREACHABLE;
}

Expand Down
2 changes: 1 addition & 1 deletion src/workerd/server/server.c++
Original file line number Diff line number Diff line change
Expand Up @@ -3162,7 +3162,7 @@ kj::Own<Server::Service> Server::makeWorker(kj::StringPtr name,
actorClasses.insert(kj::str(exportName));
}

void addWorkflowClass(kj::StringPtr exportName) override {
void addWorkflowClass(kj::StringPtr exportName, kj::Array<kj::String> methods) override {
// This is only used for validation and has no runtime implications, at least for now.
}
};
Expand Down
2 changes: 1 addition & 1 deletion src/workerd/tests/test-fixture.c++
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ struct MockErrorReporter final: public Worker::ValidationErrorReporter {

void addEntrypoint(kj::Maybe<kj::StringPtr> exportName, kj::Array<kj::String> methods) override {}
void addActorClass(kj::StringPtr exportName) override {}
void addWorkflowClass(kj::StringPtr exportName) override {}
void addWorkflowClass(kj::StringPtr exportName, kj::Array<kj::String> methods) override {}
};

inline server::config::Worker::Reader buildConfig(
Expand Down

0 comments on commit 30a5ba3

Please sign in to comment.