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

Further improve function initialization #3547

Merged
merged 4 commits into from Jul 12, 2021
Merged

Conversation

inlined
Copy link
Member

@inlined inlined commented Jul 3, 2021

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.

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.
@inlined inlined requested review from joehan and taeold July 3, 2021 01:49
@google-cla google-cla bot added the cla: yes Manual indication that this has passed CLA. label Jul 3, 2021
return;
}
for (const [keyAsString, value] of Object.entries(yaml)) {
// I don't know why Object.entries(foo)[0] isn't type of keyof foo...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL

Copy link
Contributor

@joehan joehan left a 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";
Copy link
Contributor

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?

Copy link
Member Author

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);
Copy link
Contributor

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)

Copy link
Member Author

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in brittle

Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Member Author

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", () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repeated a word here?

Copy link
Member Author

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)) {
Copy link
Contributor

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}`, () => {
Copy link
Contributor

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") {
Copy link
Contributor

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.

Copy link
Member Author

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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.

Copy link
Member Author

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)) {
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Member Author

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]);
Copy link
Contributor

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...

Copy link
Member Author

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)) {
Copy link
Contributor

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")

Copy link
Member Author

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]) {
Copy link
Contributor

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])
...

Copy link
Member Author

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,
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Member Author

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...

@inlined inlined added cla: yes Manual indication that this has passed CLA. and removed cla: yes Manual indication that this has passed CLA. labels Jul 12, 2021
@inlined inlined merged commit 0b0459b into master Jul 12, 2021
devpeerapong pushed a commit to devpeerapong/firebase-tools that referenced this pull request Dec 14, 2021
* 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
@bkendall bkendall deleted the inlined.detect-backend-yaml branch March 18, 2022 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes Manual indication that this has passed CLA.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants