Skip to content

fix: ensure the startup is completed before calling startupCompleted #7525

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

Closed

Conversation

sadortun
Copy link
Contributor

@sadortun sadortun commented Aug 27, 2021

New Pull Request Checklist

Issue Description

During #7418, i noticed that migrations were not always executed before the cloud were loaded, or serverStartComplete(), which in some case cause problem, because ParseCloud is loaded before migrations had time to complete, which can be quite bad !

This PR aims to ensure a proper sequential startup. In #7418 i will add Schema Migrations between hooksController.load() and serverStartComplete()

I've spitted this here so if we need to discuss this topic on its own.

Related issue: #7527 #7418

Approach

TODOs before merging

  • Add test cases
  • Add entry to changelog

@mtrezza
Copy link
Member

mtrezza commented Aug 27, 2021

@sadortun Thanks for discovering this bug and opening this PR!

Can you create a separate issue for this fix? #7418 is a feature request, but this seems to be a bug fix, so a separate issue would he helpful.

@dblythy
Copy link
Member

dblythy commented Sep 3, 2021

Is this ready for review @sadortun?

@sadortun
Copy link
Contributor Author

sadortun commented Sep 3, 2021

@dblythy only missing thing is to move serverStartComplete after cloud init.

I'll try to finish this and the few other open PR for next Monday.

@sadortun
Copy link
Contributor Author

sadortun commented Sep 3, 2021

@mtrezza @dblythy do the security checks need to run after anything is initialized ? Or they can be run independently?

Before, they were ran completly independently of the db/cloud init

@mtrezza
Copy link
Member

mtrezza commented Sep 3, 2021

do the security checks need to run after anything is initialized

yes

@sadortun
Copy link
Contributor Author

sadortun commented Sep 3, 2021

Currently the Security checks can run before everything is initialized.

Should I fix this here, or in a separate issue ?

For the tests to succeed, we will need a way to know the init has completed. Aka #7527

IMO, it would be best to fix/refactor the constructor init issues in one issue (#7527 and this PR) since it's all related.

@mtrezza
Copy link
Member

mtrezza commented Sep 3, 2021

Currently the Security checks can run before everything is initialized.

Are you referring to this?

if (security && security.enableCheck && security.enableCheckLog) {
new CheckRunner(options.security).run();
}

@mtrezza
Copy link
Member

mtrezza commented Sep 3, 2021

Should I fix this here, or in a separate issue ?

It's a separate issue, if it is currently an issue at all.

IMO, it would be best to fix/refactor the constructor init issues in one issue (#7527 and this PR) since it's all related.

We cannot backport breaking changes for code that is outside the scope of a vulnerability. If the constructor change is breaking, but it is not required to fix this bug, it cannot be included in the bug fix. If the constructor change is not breaking, and it reduces complexity to change the constructor together with fixing the bug because they are depending on each other, then they can be done in one PR, because we can backport them.

@sadortun
Copy link
Contributor Author

sadortun commented Sep 3, 2021

Currently the Security checks can run before everything is initialized.

Are you referring to this?

if (security && security.enableCheck && security.enableCheckLog) {
new CheckRunner(options.security).run();
}

Yes.

@sadortun
Copy link
Contributor Author

sadortun commented Sep 3, 2021

We cannot backport breaking changes for code that is outside the scope of a vulnerability. If the constructor change is breaking, but it is not required to fix this bug, it cannot be included in the bug fix. 

I really don't know if it's an issue or not, tests currently only check that the CheckRunner.run() are executed. But they can be executed before the init has completed.

I'm not going to touch it for now, and they'll keep being executed even if the init is not completed.

If it's an issue/bug/vulnerability, please open an issue.


Also the CheckRunner.run() itself is an async method, but it it's not awaited for.

@mtrezza
Copy link
Member

mtrezza commented Sep 3, 2021

I really don't know if it's an issue or not, tests currently only check that the Security check are executed. But they can be executed before the init has completed.

Depends on the checks, intuitively I would say that they should be executed after initialization, but since the tests all pass, it shouldn't be an issue now. As long as the logger is loaded, I believe the checks are covered. When adding new checks, it may become necessary to address this.

Also the CheckRunner.run() is an adync method, but it it's not awaited for.

It doesn't need await (and slow down server init), it just prints out logs.

@sadortun
Copy link
Contributor Author

sadortun commented Sep 3, 2021

Ok, perfect!

I'll update this PR to reflect what we discussed above and I'll let you know once it's done.

@mtrezza
Copy link
Member

mtrezza commented Sep 3, 2021

Amazing!

@sadortun
Copy link
Contributor Author

sadortun commented Sep 3, 2021

✅ Updated.

@mtrezza @dblythy Its ready for review.

@codecov
Copy link

codecov bot commented Sep 3, 2021

Codecov Report

Merging #7525 (0e6ecba) into alpha (b64640c) will increase coverage by 0.00%.
The diff coverage is 83.33%.

Impacted file tree graph

@@           Coverage Diff           @@
##            alpha    #7525   +/-   ##
=======================================
  Coverage   93.88%   93.88%           
=======================================
  Files         183      183           
  Lines       13624    13625    +1     
=======================================
+ Hits        12791    12792    +1     
  Misses        833      833           
Impacted Files Coverage Δ
src/ParseServer.js 90.10% <83.33%> (+0.05%) ⬆️
src/Adapters/Files/GridFSBucketAdapter.js 79.50% <0.00%> (-0.82%) ⬇️
src/RestWrite.js 94.04% <0.00%> (ø)
src/batch.js 93.10% <0.00%> (+1.72%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d35eeb8...0e6ecba. Read the comment docs.

@mtrezza
Copy link
Member

mtrezza commented Sep 3, 2021

Great, could you add a changelog entry and update the TODOs at the top?

@sadortun
Copy link
Contributor Author

sadortun commented Sep 3, 2021

Great, could you add a changelog entry and update the TODOs at the top?

Done.

@mtrezza
Copy link
Member

mtrezza commented Sep 9, 2021

Are there any open questions that need to be resolved?

@sadortun
Copy link
Contributor Author

sadortun commented Sep 9, 2021

Are there any open questions that need to be resolved?

The last discussion above, I'll try to finish It today, but I'm drowning in other stuff 🌊

@mtrezza
Copy link
Member

mtrezza commented Sep 9, 2021

@sadortun Thanks, no rush, just wanted to check what the status is. Maybe @dblythy has a chance to respond to your last comment meanwhile.

@sadortun sadortun force-pushed the pr_startupcomplete branch from 7750e52 to 015be02 Compare October 5, 2021 06:57
@sadortun
Copy link
Contributor Author

sadortun commented Oct 5, 2021

[ .... one eternity later .... ]

Here you go !

  • ✅ Resolved discussions
  • ✅ Rebased

@mtrezza
Copy link
Member

mtrezza commented Oct 8, 2021

Is this ready for review? @dblythy could you take a look?
@sadortun would you be available to do PR reviews at times? If so, I would add you to the server review team that gets notifications when PRs are ready for review.

@sadortun
Copy link
Contributor Author

sadortun commented Oct 8, 2021

would you be available to do PR reviews at times

Yeah sure !

@mtrezza mtrezza changed the title Ensure the startup is completed before calling startupCompleted fix: ensure the startup is completed before calling startupCompleted Oct 9, 2021
@parse-github-assistant
Copy link

Thanks for opening this pull request!

  • 🎉 We are excited about your hands-on contribution!

@dblythy dblythy self-requested a review October 9, 2021 00:37
@mtrezza
Copy link
Member

mtrezza commented Oct 9, 2021

@sadortun is there any way we can add tests for this? that should also solve the coverage decrease.

Copy link
Member

@dblythy dblythy left a comment

Choose a reason for hiding this comment

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

Looks mostly good couple of minor changes.

I have addressed the coverage issues in my cloud code PR (#7564)

@mtrezza
Copy link
Member

mtrezza commented Oct 13, 2021

What do we need to merge this?

@Moumouls
Copy link
Member

Moumouls commented Oct 14, 2021

Here @sadortun i think you only need to move

The cloud code loading is sync, then we are safe to execute init

  if (cloud) {
      addParseCloud();
      if (typeof cloud === 'function') {
        cloud(Parse);
      } else if (typeof cloud === 'string') {
        if (process.env.npm_package_type === 'module') {
          import(path.resolve(process.cwd(), cloud));
        } else {
          require(path.resolve(process.cwd(), cloud));
        }
      } else {
        throw "argument 'cloud' must either be a string or a function";
      }
    }

before

 databaseController
      .performInitialization()
      .then(() => hooksController.load())
      .then(async () => {
        if (schema) {
          await new DefinedSchemas(schema, this.config).execute();
        }
        if (serverStartComplete) {
          serverStartComplete();
        }
      })
      .catch(error => {
        if (serverStartComplete) {
          serverStartComplete(error);
        } else {
          console.error(error);
          process.exit(1);
        }
      });

I'll also take a deeper look to the init on the defined schema PR

EDIT:

Since on defined schema PR we have

.then(async () => {
        if (schema) {
          await new DefinedSchemas(schema, this.config).execute();
        }
        if (serverStartComplete) {
          serverStartComplete();
        }
      })

Here just moving up the cloud code register should be okay. Make sense if a user wants to trigger a cloud code function in the beforeMigration function.

@sadortun i think you can easily write a failing test, trying to use a cloud function into the beforeMigration function once the defined schema PR is merged.

Also since this issue is related to the defined schema feature, here i suggest to close this PR and add directly the fix + test on the defined schema PR. @mtrezza @dblythy @sadortun what do you think

@Moumouls
Copy link
Member

Moumouls commented Oct 14, 2021

Also here the correct structure to ensure proper startup of Parse Server.

const server = new Promise((resolve) => {
const parseServer =  ParseServer.start({ serverStartComplete: () => {
  resolve(parseServer)
// Add a dedicated health check endpoint if you want
  parseServer.expressApp.get('/ready', (req: any, res: any) => {
	res.send('true')
  })
  })
})
await server
// Server is loaded
console.log('Started')

@sadortun exemple code herer https://github.com/Moumouls/next-atomic-gql-server/blob/master/src/server.ts

May be here you are right about returning a promise for ParseServer.start, it will be easy to await server start.

const server = await ParseServer.start()
console.log('Server started')

Refactoring the parse server start to async/await will solve the issue and aslo allow to remove the old .then syntax

@mtrezza
Copy link
Member

mtrezza commented Nov 6, 2021

Note this this PR (like any other) must not contain any breaking changes without deprecation, see #7680 (comment).

@mtrezza
Copy link
Member

mtrezza commented Nov 6, 2021

@sadortun could you merge alpha into this and resolve any conflicts?

@Moumouls could you give this a quick review? you mentioned some feedback regarding this yesterday. and maybe we can close (or continue) the open questions above.

Copy link
Member

@Moumouls Moumouls left a comment

Choose a reason for hiding this comment

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

Hi @sadortun @dblythy @mtrezza

After a after a careful reading, this PR will introduce a new race issue. We have a similar issue like the import.

Currently ParseServer was always started in a sync way. Cloud code is also loaded in sync.

Here the PR start to load the code (even with require) in async since require is now in a then function.

So this PR introduce a race and also a breaking change if some developers do not use serverStartComplete to correctly await the full start.

Here we cannot solve the import race issue. Also to keep this PR focused on the original issue, i'll recommend to just move the old

    if (cloud) {
      addParseCloud();
      if (typeof cloud === 'function') {
        cloud(Parse);
      } else if (typeof cloud === 'string') {
        if (process.env.npm_package_type === 'module') {
          import(path.resolve(process.cwd(), cloud));
        } else {
          require(path.resolve(process.cwd(), cloud));
        }
      } else {
        throw "argument 'cloud' must either be a string or a function";
      }
    }

Before

databaseController
      .performInitialization()

Then ParseServer will load correctly in this order:

  • load cloud code (sync with require, same JS loop as the Parse Server constructor)
  • start db init
  • after db init load hooks
  • after load hooks load DefinedSchemas
  • after defined schemas, call server Start complete

@sadortun
Copy link
Contributor Author

sadortun commented Nov 8, 2021

@Moumouls

Your suggestions make sense,

Also just to clarify, the current [Parse release] init may or may not load CloudCode before or after the DB init sequence, since the DB init is made async.

Moving the CloudCode init before would make the whole init process deterministic, which is good.

Could it be possible that any user rely on the possibility that when CloudCode is loaded, DB was already initialized? I don't think so, but it might be something to consider if it could be a BC break if we fix this inconsistency.

@sadortun
Copy link
Contributor Author

sadortun commented Nov 10, 2021

@mtrezza do you still plan in merging this (once updated)? I think I saw you wanted to wait in 2022?

LMK, I have some time today to finish it so it could be ready later today.


This PR should not introduce any BC break

@mtrezza
Copy link
Member

mtrezza commented Nov 11, 2021

If there is no breaking change, we can merge this also into beta, as it seems to contain a fix. Is there anything that needs to change in this PR to incorporate the feedback above? Could you bring this up-to-date with alpha?

@sadortun
Copy link
Contributor Author

Is there anything that needs to change in this PR to incorporate the feedback above?

Yep, I'll do that in ~1-2 hours. @Moumouls prochaine vides really great feedback

Could you bring this up-to-date with alpha?

Will do.

@Moumouls
Copy link
Member

Yep, I'll do that in ~1-2 hours. @Moumouls prochaine vides really great feedback

I try to do my best here 🙂

I'll take a look here

Comment on lines +97 to 117
Promise.resolve()
.then(async () => await databaseController.performInitialization())
.then(async () => await hooksController.load())
.then(async () => {
if (schema) {
await new DefinedSchemas(schema, this.config).execute();
}
})
.then(async () => {
if (serverStartComplete) {
serverStartComplete();
await serverStartComplete();
}
})
.catch(error => {
.catch(async (error) => {
if (serverStartComplete) {
serverStartComplete(error);
await serverStartComplete(error);
} else {
console.error(error);
process.exit(1);
}
});
Copy link
Member

Choose a reason for hiding this comment

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

Just to help to cross de finish line, here the full correct structure.

    // Note: Tests will start to fail if any validation happens after this is called.
    databaseController.performInitialization()
      .then(() => hooksController.load())
      .then(async () => {
        if (schema) {
          await new DefinedSchemas(schema, this.config).execute();
        }
      })
      .then(async () => {
        if (serverStartComplete) {
          await Promise.resolve(serverStartComplete());
        }
      })
      .catch(async (error) => {
        if (serverStartComplete) {
          await Promise.resolve(serverStartComplete(error));
        } else {
          console.error(error);
          process.exit(1);
        }
      });

Then in a futur PR that refactor the start up (BK change) the structure will be

try {
  await databaseController.performInitialization()
  await hooksController.load()
  if (schema) {
    await new DefinedSchemas(schema, this.config).execute();
  }
  if (serverStartComplete) {
    await Promise.resolve(serverStartComplete());
  }
} catch(e){
  if (serverStartComplete) {
      await Promise.resolve(serverStartComplete(error));
  } else {
    console.error(error);
    process.exit(1);
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Mmm, not sure what's the difference.

I don't remember where, but we had a discussion about starting Promises chains with Promise.resolve() to be consistent with other promises chains in Server and JS SDK

  1. await Promise.resolve(serverStartComplete());

I don't think we need to wrap it around Promise.resolve, await will work either way.

@mtrezza
Copy link
Member

mtrezza commented Nov 11, 2021

Just a reminder that breaking changes are phased-in. So if we know already that a breaking change will be needed in the future, this PR should already deprecate the current approach and provide the new approach optionally.

@dblythy
Copy link
Member

dblythy commented Dec 23, 2022

Closing as this feature is now part of the alpha branch

@dblythy dblythy closed this Dec 23, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants