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
Further improve function initialization #3547
Conversation
The primary purpose of this change is to introduce the "discovery" subpackage of "runtimes". This package is a common utility that all runtimes will use to discover their backend spec based on the "container contract" being finalized currently. The container contract says that we must interact with customer code using tools we'd have in a docker container: a fully-contained directory of code, a "run" command, and environment variables. Backend specs can be discovered as: 1. The file /backend.yaml at the root of the container 2. The response to http://localhost:${ADMIN_PORT}/backend.yaml when running the container's RUN command with ADMIN_PORT set to a free port. The golang runtime fulfills #2 through two steps: 1. The Functions SDK includes the "codegen" package that reads the customer code and creates an "autogen" main package that runs the customer code as the Firebase runtime (skeleton implementation currently). 2. We then start up the Firebase runtime with ADMIN_PORT set and fetch /backend.yaml After this + a minor fix to the template code, we have working Go deploys! I also moved gomod parsing into its own file to keep index.ts to a minimal set of things that aren't easily unit tested. Unfortunately the diff is largr than it needs to be now.
return; | ||
} | ||
for (const [keyAsString, value] of Object.entries(yaml)) { | ||
// I don't know why Object.entries(foo)[0] isn't type of keyof foo... |
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.
Interesting read at microsoft/TypeScript#12253 (comment)
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.
TIL
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.
Some nitty things, but this mostly LGTM
|
||
// Use "omit" for output only fields. This allows us to fully exhaust keyof T | ||
// while still recognizing output-only fields | ||
export type Type = "string" | "number" | "boolean" | "object" | "array" | "omit"; |
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.
Nit: type already has a meaning in ts, so I don't love us making a new meaning for Type. Consider KeyType 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.
Sure, why not?
region: string, | ||
runtime: runtimes.Runtime | ||
): backend.Backend { | ||
const bkend: backend.Backend = _.cloneDeep(yaml); |
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.
Could we avoid using lodash here? I think JSON.parse(JSON.stringify()) achieves the same things natively, and is only slightly worse in performance (https://measurethat.net/Benchmarks/Show/2751/0/lodash-clonedeep-vs-json-clone)
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 leaves me feeling dirty, but so does lodash.
// Now that we've created main.go, we will have new dependencies on the packages pulled in | ||
// by /support/runtime. We need to run one more `go get`. We could have possibly frontloaded | ||
// this work by adding a hand curated list of dependencies in project creation, but this | ||
// would be more birttle. |
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.
Typo in brittle
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.
To make sure I understand, this would be more brittle because it would mean that project creation would fail if we couldn't get one of the dependencies?
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've added more flavor to the text. It's brittle because we wouldn't handle the case where the autogen code added a new dependency in some SDK version that comes out after the customer's CLI was published.
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.
Actually, when I tried a second time I was able to get everything working by go getting the subpackages in the firebase init code. Removed this for now. It bothers me how fragile this code can seem.
import * as parsing from "../../../../../deploy/functions/runtimes/discovery/parsing"; | ||
|
||
describe("requireKeys", () => { | ||
it("accepts found keys keys", () => { |
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.
repeated a word here?
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.
yup yup
}; | ||
for (const type of tests) { | ||
const schema = { [type]: type as parsing.Type }; | ||
for (const [testType, val] of Object.entries(values)) { |
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.
Very clever!
for (const type of tests) { | ||
const schema = { [type]: type as parsing.Type }; | ||
for (const [testType, val] of Object.entries(values)) { | ||
it(`Handles a ${testType} when expecting a ${type}`, () => { |
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.
Nit: descriptions should start with lowercase
if (!yaml.specVersion) { | ||
throw new FirebaseError("Expect backend yaml to specify a version number"); | ||
} | ||
if (yaml.specVersion === "v1alpha1") { |
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 we want to follow AIP stability levels this should be v1alpha
not v1alpha1
...do you intend for the spec version to match API version, or do you think they should be versioned separately? I can see argument for both.
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 was actually wondering if we wanted semver or stability levels. As is, these things are strictly parsed, so there is no such thing as a revision bump. I'm open to other suggestions though.
} | ||
|
||
logger.debug("Found backend.yaml. Got spec:", text); | ||
// TOODO(inlined): use a schema instead of manually checking everything or blindly trusting input. |
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.
// TOODO(inlined): use a schema instead of manually checking everything or blindly trusting input. | |
// TODO(inlined): use a schema instead of manually checking everything or blindly trusting input. |
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.
Removing comment entirely since I actually added validation post-hackathon.
break; | ||
} catch (err) { | ||
// Allow us to wait until the server is listening. | ||
if (/ECONNREFUSED/.exec(err?.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.
Probably want some amount of backoff or delay here, right?
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.
Also, is this the correct way to check for error types? Is this available instead as err?.code
without having to regexp it? Even if not, I probably prefer err?.message?.includes("ECONNREFUSED")
to inline regexp exec.
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.
Yes, I keep forgetting about err?.code
|
||
while (true) { | ||
try { | ||
res = await Promise.race([fetch(`http://localhost:${port}/backend.yaml`), timedOut]); |
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 don't think this will work if an ECONNREFUSED
actually occurs, will it? Is it safe to use the same promise in multiple Promise.race
calls?
I would think you'd need to race the timeout against an async function that does the looping retry logic...
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 verified to work. Without the race + retry the curl will fail while the harness is booting.
try { | ||
await promisify(fs.mkdir)(path.join(this.sourceDir, "autogen")); | ||
} catch (err) { | ||
if (!/EEXIST/.exec(err?.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.
Again if err?.code
is available that feels better, and if not I think I prefer err?.message?.includes("EEXIST")
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.
Agreed, using code
prefix = prefix + "."; | ||
} | ||
for (const key of keys) { | ||
if (!yaml[key]) { |
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.
Was this meant to be:
const fullKey = prefix ? prefix + "." + key : key;
if (!yaml[fullKey])
...
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.
Prefix is used below in the error message. The use case is:
const obj = {
outer: {
inner: 5,
}
}`
requireKeys("outer", obj.outer, "inner");
In this case, the error message should be about "outer.inner", but the context object is just {inner: 5}
so the key shouldn't have the prefix.
const childProcess = spawn("go", ["run", "./autogen"], { | ||
env: { | ||
...envs, | ||
...process.env, |
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 inheriting process.env
intended? I would've thought that we won't want to do this in anticipation of a future where parsing is done on the server side.
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.
You're right, I should remove this and keep playing reverse-whack-a-mole to get all the envs I was otherwise missing.
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 turns out the magic env is HOME
. Without HOME
set all go
commands fail with the error error missing $GOPATH
...
…ebase-tools into inlined.detect-backend-yaml
* Further improve function initialization The primary purpose of this change is to introduce the "discovery" subpackage of "runtimes". This package is a common utility that all runtimes will use to discover their backend spec based on the "container contract" being finalized currently. The container contract says that we must interact with customer code using tools we'd have in a docker container: a fully-contained directory of code, a "run" command, and environment variables. Backend specs can be discovered as: 1. The file /backend.yaml at the root of the container 2. The response to http://localhost:${ADMIN_PORT}/backend.yaml when running the container's RUN command with ADMIN_PORT set to a free port. The golang runtime fulfills firebase#2 through two steps: 1. The Functions SDK includes the "codegen" package that reads the customer code and creates an "autogen" main package that runs the customer code as the Firebase runtime (skeleton implementation currently). 2. We then start up the Firebase runtime with ADMIN_PORT set and fetch /backend.yaml After this + a minor fix to the template code, we have working Go deploys! I also moved gomod parsing into its own file to keep index.ts to a minimal set of things that aren't easily unit tested. Unfortunately the diff is largr than it needs to be now. * PR feedback
The primary purpose of this change is to introduce the "discovery"
subpackage of "runtimes". This package is a common utility that all
runtimes will use to discover their backend spec based on the "container
contract" being finalized currently.
The container contract says that we must interact with customer code
using tools we'd have in a docker container: a fully-contained directory
of code, a "run" command, and environment variables. Backend specs can
be discovered as:
running the container's RUN command with ADMIN_PORT set to a free
port.
The golang runtime fulfills #2 through two steps:
customer code and creates an "autogen" main package that runs
the customer code as the Firebase runtime (skeleton implementation
currently).
fetch /backend.yaml
After this + a minor fix to the template code, we have working Go
deploys!
I also moved gomod parsing into its own file to keep index.ts to a
minimal set of things that aren't easily unit tested. Unfortunately
the diff is largr than it needs to be now.