-
Notifications
You must be signed in to change notification settings - Fork 779
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
startDevWorker - Milestone 1 #3960
Conversation
🦋 Changeset detectedLatest commit: 334bfa0 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
a179b91
to
51f8854
Compare
@@ -60,7 +60,7 @@ | |||
"emit-types": "tsc -p tsconfig.emit.json && node -r esbuild-register scripts/emit-types.ts", | |||
"prepublishOnly": "SOURCEMAPS=false npm run build", | |||
"start": "pnpm run bundle && cross-env NODE_OPTIONS=--enable-source-maps ./bin/wrangler.js", | |||
"test": "pnpm run assert-git-version && jest", | |||
"test": "pnpm run assert-git-version && jest --runInBand", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had to do this to avoid a race condition where every test thought it could start the InspectorProxyWorker
on 9229
because get-port
doesn't work across processes (jest workers, in this case)
We confirmed this affects wrangler dev
and unstable_dev
if you run multiple processes in parallel where get-port
thinks the requested port is available but then there's a race for all the workerd instances to bind to it
We have a fix coming in a follow-up
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: make miniflare surface "port in use" errors so wrangler can retry instantiating Miniflare with port 0
@@ -69,6 +70,7 @@ export interface UnstableDevOptions { | |||
export interface UnstableDevWorker { | |||
port: number; | |||
address: string; | |||
proxyData: ProxyData; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding to this public interface so we can fake some startDevWorker events (call the event listeners)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO add getBindings to this interface
/** | ||
* @internal | ||
*/ | ||
export class DevEnv extends EventEmitter { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class contains the glue code for all startDevWorker controllers. This is the contract between all controllers. You could override the instances of each/any controller in the constructor but we're not exposing this to users yet until we're more sure of the contract.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First pass, tentatively, looks GREAT! I will probably go over it again maybe even jump on a call with @RamIdeas for a pair review for a 2nd pass!
This PR also should have a couple approvals minimum.
@@ -0,0 +1,41 @@ | |||
import type Protocol from "devtools-protocol/types/protocol-mapping"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The types from devtools-protocol don't expose the full object we see in the websocket messages, just the sub-properties for some reason
This file may seem daunting but it just coerces the types into the whole object we see – e.g. with method
and params
matched together not as separate type unions which is basically useless
@mrbbot got nerd-sniped and got it working 👏
@@ -244,7 +255,55 @@ type DevSessionProps = DevProps & { | |||
}; | |||
|
|||
function DevSession(props: DevSessionProps) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is (essentially) the entry-point to wrangler dev
– here we create a DevEnv
and map the props into StartDevWorkerOptions
(at least what is relevant for now – for ProxyController) and fake events by calling ProxyController
event handler methods (devEnv.proxy.on*
)
initialPort={0} // hard-code for userworker, DevEnv-ProxyWorker now uses this prop value | ||
initialIp={"127.0.0.1"} // hard-code for userworker, DevEnv-ProxyWorker now uses this prop value | ||
rules={props.rules} | ||
inspectorPort={props.inspectorPort} | ||
runtimeInspectorPort={props.runtimeInspectorPort} | ||
localPersistencePath={props.localPersistencePath} | ||
liveReload={props.liveReload} | ||
crons={props.crons} | ||
queueConsumers={props.queueConsumers} | ||
localProtocol={props.localProtocol} | ||
localProtocol={"http"} // hard-code for userworker, DevEnv-ProxyWorker now uses this prop value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The user worker now never starts on https, and binds to 127.0.0.1
on a random port. The ProxyWorker will start on the previous address values
usePreviewServer({ | ||
previewToken, | ||
assetDirectory: props.isWorkersSite | ||
? undefined | ||
: props.assetPaths?.assetDirectory, | ||
localProtocol: props.localProtocol, | ||
localPort: props.port, | ||
ip: props.ip, | ||
}); | ||
|
||
useInspector({ | ||
inspectorUrl: | ||
props.inspect && previewToken | ||
? previewToken.inspectorUrl.href | ||
: undefined, | ||
port: props.inspectorPort, | ||
logToTerminal: true, | ||
sourceMapPath: props.sourceMapPath, | ||
host: previewToken?.host, | ||
name: props.name, | ||
sourceMapMetadata: props.bundle?.sourceMapMetadata, | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No more preview server or inspector server, replaced by ProxyWorker and ProxyInspectorWorker respectively
@@ -72,6 +74,34 @@ export async function startDevServer( | |||
} | |||
} | |||
|
|||
const devEnv = new DevEnv(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This outer function (startDevServer) is called internally by unstable_dev. Here we create a DevEnv and fake events to the ProxyController by calling it's event handler methods (devEnv.proxy.on*
)
// `selfsigned` imports `node-forge`, which is a pretty big library. | ||
// To reduce startup time, only load this dynamically when needed. | ||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports | ||
const generate: typeof import("selfsigned").generate = promisify( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is one of those fake async libs where they provided a node.js callback signature for ecosystem consistency. It makes no difference to call it without a callback ("synchronously") but this way we can de-async the callstack
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, could we take advantage of the existing async nature of this function to use an ESM await import()
? Reducing synchronous require()
s will be helpful with things like e.g. a future move to Viteste
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think await import()
segfaults in Jest, breaking our existing tests 🙁
@@ -0,0 +1,4 @@ | |||
declare module "worker:*" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Declaring the type for worker:
prefix module imports. We added an esbuild plugin to find these worker files in packages/wrangler/templates/*
this.requestQueue.set(request, promise); | ||
this.processQueue(); | ||
|
||
return promise; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Synchronously returning this promise. The promise will resolve later when we process the queue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any possible things running that would depend on this Promise? Considering possible race conditions or the like.
processProxyControllerRequest(request: Request) { | ||
const event = request.cf?.hostMetadata; | ||
switch (event?.type) { | ||
case "pause": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pause
events cause the ProxyWorker to buffer requests
this.proxyData = undefined; | ||
break; | ||
|
||
case "play": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
play
events cause the ProxyWorker to begin proxying requests to the UserWorker (using proxyData)
await env.PROXY_CONTROLLER.fetch("http://dummy", { | ||
method: "POST", | ||
body: JSON.stringify(message), | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ProxyWorker can communicate back to the ProxyController via this service binding
} | ||
} | ||
|
||
void fetch(url, new Request(req, { headers })) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not awaiting this fetch means we replay the requests in order but we don't guarantee their delivery in order. We could await just the headers (not the body) to guarantee delivery order too but I don't think it matters too much – happy to hear other opinions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you are chaining then & catch, why is the void
necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It shouldn't be a floating promise or anything, if its been chained with a catch I mean.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just being explicit about not awaiting the promise since we're in a loop and it's on purpose
But I guess I could just add a comment instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason not to await the fetch? We're already creating an ordered buffer of requests, we might as well make sure that the delivery order is preserved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main reason is that if the UserWorker has any long-running requests and therefore requires parallel request handling, await-ing would prevent that.
I've left a comment stating, if we choose to await, we should also race it against a timeout of e.g. 100ms
res = insertLiveReloadScript(req, res, this.env, proxyData); | ||
} | ||
|
||
deferred.resolve(res); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This resolves the promise returned by the initial fetch handler
}, | ||
}); | ||
|
||
deferred.reject(error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This rejects the promise returned by the initial fetch handler
packages/wrangler/templates/startDevWorker/InspectorProxyWorker.ts
Outdated
Show resolved
Hide resolved
}, | ||
} as ExportedHandler<Env>; | ||
|
||
export class InspectorProxyWorker implements DurableObject { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a big boi, sorry
It's job is to proxy messages between the runtime inspector (#websockets.runtime
), ProxyController (#websockets.proxyController
) and devtools client (#websockets.devtools
).
Because only one client can be connected to the runtime inspector server, the InspectorProxyWorker is essentially that one client. The ProxyController and devtools then can both connect to the InspectorProxyWorker.
247cf2c
to
8863445
Compare
for ProxyController -> (Inspector)ProxyWorker requests to aid debugging
without requiring ProxyWorker restart
|
||
// if passed a previousDeferred, ensure it is resolved with the newDeferred | ||
// so that await-ers of previousDeferred are now await-ing newDeferred | ||
previousDeferred?.resolve(newPromise); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit unclear on the use-case for chaining deferreds. If a deferred gets inherited, any holder of the previous deferred loses control over its lifecycle (the resolvers become inert since its promise has now already been resolved). Similarly, if the previous deferred has already been resolved before it gets inherited, the new deferred isn't allowed to extend the lifecycle of the previous one.
So whether or not a holder of a deferred maintains control over its lifecycle, and whether or not its lifecycle has been extended is unreliable. This seems error-prone and like it would lead to some insidious bugs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a deferred gets inherited, any holder of the previous deferred loses control over its lifecycle (the resolvers become inert since its promise has now already been resolved)
True
Similarly, if the previous deferred has already been resolved before it gets inherited, the new deferred isn't allowed to extend the lifecycle of the previous one.
True
So whether or not a holder of a deferred maintains control over its lifecycle, and whether or not its lifecycle has been extended is unreliable. This seems error-prone and like it would lead to some insidious bugs.
The point is not to extend the lifecycle of the old promise. It's to ensure the old promise is not left uncompleted. If the old promise is already completed (resolved/rejected), this line has no effect
What this PR solves / how to test:
This PR implements the first milestone for
startDevWorker
. This includes the ProxyController, ProxyWorker and ProxyInspectorWorker. ThestartDevWorker
function is not exposed yet – the components implemented so far have been inserted tactfully 👀 into the existing flows forwrangler dev
,wrangler dev --remote
,unstable_dev(...)
.There should be no breaking changes. Improvements include:
Author has included the following, where applicable:
Reviewer is to perform the following, as applicable:
Note for PR author:
We want to celebrate and highlight awesome PR review! If you think this PR received a particularly high-caliber review, please assign it the label
highlight pr review
so future reviewers can take inspiration and learn from it.