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

feat: create await new ParseServer(config).startApp() to resolve cloud code and schema before express app is exposed #7914

Closed
wants to merge 53 commits into from

Conversation

dblythy
Copy link
Member

@dblythy dblythy commented Mar 31, 2022

New Pull Request Checklist

Issue Description

Currently, there is no way to use cloud code if you are using ES Modules due to the fact that import needs to be async.

Related issue: #7560
Closes: #7527, #7560

Approach

Adds await new ParseServer().startApp() that create a Parse Server asynchronously so that a developer using ES imports and schemas can be sure that their cloud triggers and schemas are registered before creating the express app.

TODOs before merging

  • Add tests
  • A changelog entry is created automatically using the pull request title (do not manually add a changelog entry)

mtrezza and others added 12 commits March 25, 2022 19:47
## [5.2.1-alpha.1](parse-community/parse-server@5.2.0...5.2.1-alpha.1) (2022-03-26)

### Bug Fixes

* return correct response when revert is used in beforeSave ([parse-community#7839](parse-community#7839)) ([f63fb2b](parse-community@f63fb2b))
## [5.2.1-alpha.2](parse-community/parse-server@5.2.1-alpha.1...5.2.1-alpha.2) (2022-03-26)

### Performance Improvements

* reduce database operations when using the constant parameter in Cloud Function validation ([parse-community#7892](parse-community#7892)) ([48bd512](parse-community@48bd512))
# [5.3.0-alpha.2](parse-community/parse-server@5.3.0-alpha.1...5.3.0-alpha.2) (2022-03-27)

### Bug Fixes

* security upgrade parse push adapter from 4.1.0 to 4.1.2 ([parse-community#7893](parse-community#7893)) ([ef56e98](parse-community@ef56e98))
@parse-github-assistant
Copy link

parse-github-assistant bot commented Mar 31, 2022

Thanks for opening this pull request!

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

@dblythy dblythy marked this pull request as draft March 31, 2022 07:55
@codecov
Copy link

codecov bot commented Mar 31, 2022

Codecov Report

Base: 94.21% // Head: 85.04% // Decreases project coverage by -9.17% ⚠️

Coverage data is based on head (c29ba4f) compared to base (3b775a1).
Patch coverage: 95.45% of modified lines in pull request are covered.

Additional details and impacted files
@@            Coverage Diff             @@
##            alpha    #7914      +/-   ##
==========================================
- Coverage   94.21%   85.04%   -9.18%     
==========================================
  Files         182      182              
  Lines       13733    13762      +29     
==========================================
- Hits        12939    11704    -1235     
- Misses        794     2058    +1264     
Impacted Files Coverage Δ
src/Options/Definitions.js 100.00% <ø> (ø)
src/Options/index.js 100.00% <ø> (ø)
src/Config.js 88.72% <50.00%> (-0.30%) ⬇️
src/ParseServer.js 91.74% <97.22%> (+1.69%) ⬆️
src/SchemaMigrations/DefinedSchemas.js 88.77% <100.00%> (+0.12%) ⬆️
...dapters/Storage/Postgres/PostgresStorageAdapter.js 2.48% <0.00%> (-93.23%) ⬇️
src/Adapters/Storage/Postgres/PostgresClient.js 5.00% <0.00%> (-65.00%) ⬇️
src/Controllers/UserController.js 95.34% <0.00%> (-2.33%) ⬇️
src/Controllers/FilesController.js 92.00% <0.00%> (-2.00%) ⬇️
src/batch.js 92.98% <0.00%> (-1.76%) ⬇️
... and 5 more

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report at Codecov.
📢 Do you have feedback about the report comment? Let us know in this issue.

@dblythy dblythy marked this pull request as ready for review March 31, 2022 09:28
@mtrezza
Copy link
Member

mtrezza commented Mar 31, 2022

Restarted failed tests

@dblythy dblythy closed this Mar 31, 2022
@dblythy dblythy reopened this Mar 31, 2022
src/ParseServer.js Outdated Show resolved Hide resolved
@dblythy
Copy link
Member Author

dblythy commented Apr 5, 2022

I think this might close #7527

@dblythy dblythy changed the title fix: allow ES import for cloud string if package type is module, create await new ParseServer(config).startApp() feat : allow ES import for cloud string if package type is module, create await new ParseServer(config).startApp() Apr 28, 2022
@dblythy dblythy changed the title feat : allow ES import for cloud string if package type is module, create await new ParseServer(config).startApp() feat: create await new ParseServer(config).startApp() to resolve cloud code and schema before express app is exposed Apr 28, 2022
@dblythy
Copy link
Member Author

dblythy commented Apr 28, 2022

It’s a bit of everything really. And yep, I believe the race condition is fixed by this PR. @Moumouls might have more to say though

@Moumouls
Copy link
Member

Hi there @mtrezza @dblythy,

This PR also resolves the issue of the server being exposed before schemas are loaded.

Perfect, it was also a point that I planned to check to support correctly serverless env.

Here I only have one question; if you expose the app port AFTER the migration/init/serverStartComplete, what happens if a developer uses the Parse JS SDK into these functions (before/after migration, startServerComplete). From my knowledge, if I remember correctly the Parse JS SDK onboarded on parse-server use the "serverUrl" to interact with parse-server. So a developer could be blocked (also if he use the rest/graphql api in these functions).

To avoid this issue on my side for serverless env, I found that the we should introduce a new "publicPort" option like the "publicServerUrl" to fix the issue, and only expose parse-server to external traffic once it's ready.

We should not use a "directAccess" strategy, since developers could use GraphQL API or the REST API in these initialization functions.

If you confirm with a test that Parse JS SDK (or rest call, or Graphql call) query into before/after migration /startServerComplete fails, your changes will be a breaking change. And we will need to add a new "publicPort" to expose the completely initialized parse-server to the external traffic.

Tell what do you think about this @dblythy 🙂

Comment on lines 175 to 178
async createDeleteSession() {
const session = new Parse.Session();
await session.save(null, { useMasterKey: true });
await session.destroy({ useMasterKey: true });
const { response } = await rest.create(this.config, Auth.master(this.config), '_Session', {});
await rest.del(this.config, Auth.master(this.config), '_Session', response.objectId);
}
Copy link
Member

Choose a reason for hiding this comment

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

Owww I see here @dblythy that you encountered the error since the SDK uses the port.

Copy link
Member

Choose a reason for hiding this comment

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

the old code should not be changed, if the old code works, any Parse SDK/GraphQL/REST call during the init will work.

src/ParseServer.js Show resolved Hide resolved
src/ParseServer.js Outdated Show resolved Hide resolved
src/ParseServer.js Outdated Show resolved Hide resolved
@dblythy
Copy link
Member Author

dblythy commented Apr 28, 2022

The old code works fine because the core tests don’t care for race conditions / deploying schema before exposure and checking cloud code, and use the current function ParseServer.start. This results in Parse mounting on its express app as soon as it’s created so any endpoint is available, before schemas, cloud code etc, which is problematic in envs which re-scale often. If you have a beforeSave or beforeFind hook that ensures data security and Parse exposes that route before the trigger is registered, then it can lead to big leaks if new instances are regularly deployed.

This new feature isn’t a breaking change as existing functionality works fine, if you want Parse Server to wait to init before your express app you can await the new method, otherwise you can leave as is. No migration is required, only recommended for scaling deployments.

Any JS SDK methods will only work when .listen is called, but if someone is going to the effort to use await new ParseServer().startApp(), I would argue they would expect it to await and only be available when the mount it to their express app.

My thoughts is that there isn’t any downside to the new function, there will be some improvements to be made for sure but I don’t see why it can’t operate as is alongside the current implementation. Anyone who would implement it would be aware that the server won’t be available until they mount it with express.

@Moumouls
Copy link
Member

The old code works fine because the core tests don’t care for race conditions

Yes but the new start-up strategy will not expose parse-server port until startServerComplete for example. In my case for example I've some Rest/GraphQL calls during serverStartComplete to seed/perform DB checks before exposing the app.

Also, users that want to use the before/afterMigration function and try to request some data will be blocked.

Parse Server historically has 2 serverUrl properties, one for public traffic, and another one for internal traffic. serverUrl and publicServerUrl. I think we can introduce the same strategy with "publicPort"

You can easily call listen 2 times on express app :)

before/afterMigration, serverStartComplete function without the possibility to call Parse REST/GraphQL/SDK API seems useless to me.

Also, you can test the breaking change by trying to call a Parse.Query into the serverStartComplete. it should fail currently.

@mtrezza
Copy link
Member

mtrezza commented Apr 29, 2022

In my case for example I've some Rest/GraphQL calls during serverStartComplete to seed/perform DB checks before exposing the app.

Could you give an example? Does this mean you want to be able to call the Parse Server API before the server has finished its start-up procedure?

@mtrezza mtrezza force-pushed the alpha branch 2 times, most recently from 59215e6 to e6d7d8f Compare May 1, 2022 02:29
@dblythy
Copy link
Member Author

dblythy commented May 3, 2022

Does this mean you want to be able to call the Parse Server API before the server has finished its start-up procedure?

I'm not quite sure how this is at all possible in the current version, considering in order to call the Parse API the public routes have to be available, and app.listen is called. If any "startup" procedures currently require the JS API, then all API routes are publicly available (as Parse Server has been mounted).

To circumvent this, I have added a Parse Server option holdPublicRoutes. If this is set to true, the Parse API will require a valid masterKey until mountPublicRoutes() is called. This allows for:

const config = {
   ...
  holdPublicRoutes: true,
}
const parseServer = await new ParseServer(config).startApp();
const app = express();
app.use('/parse', parseServer);
const server = app.listen(12701);
await longStartupMethod();
parseServer.mountPublicRoutes(); // any client API calls before this will return unauthorized: master key is required

@Moumouls
Copy link
Member

Moumouls commented May 3, 2022

@dblythy @mtrezza I apologize. I think I found the misunderstanding here 🙂

From the beginning, on my side, I was talking about the static start() ParseServer method. The static start method calls the app.listen in the same NodeJS event loop as the ParseServer constructor. This behavior exposes the app before the server is initialized and allows a developer to perform queries on an initializing server.

The static start method should be maybe refactored/deprecated in favor of a static async start, to only listen once the server is initialized. The developers can await the start or use serverStartComplete.
In the static async start use case, the listen SHOULD occur, after before if (serverStartComplete) {. Then developers will be able to perform queries in "serverStartComplete".

I use the static start method on my projects combined with serverStartComplete: ex: https://github.com/Moumouls/parse-next-mono-starter/blob/65d2a9e6d485185c1164619e17d64d4d45034d88/packages/back/src/server.ts#L75

I was also wrong to want parse-server to be able to handle readiness by itself. I think we should not implement options like mountPublicRoutes(); or publicPort. After a quick check of some process app managers/orchestrators, like PM2, Kubernetes, Knative, GCP Cloud Run, and other hosting services, readiness probes are handled very differently. It's out of the scope of parse-server to be able to emit/handle correctly these readiness probes, public traffic routing availability.

Finally, sorry for the wasted time here. I can suggest removing the mountPublicRoutes options, and to add the static version of startAsync with the call to listen() BEFORE the startServerComplete in case of a startup initiated from the static call (this is the current behavior of parse-server). Then developers will be able to implement their readiness probes, code business logic, after the await startAsync() or in the serverStartComplete

From the static start and startAsync method the events should occur in this order from my point of view:

  • performInitialization
  • hooksController.load()
  • await new DefinedSchemas(schema, this.config).execute();
  • (in case of static start: call app.listen here)
  • await Promise.resolve(serverStartComplete())
  • startCallbackSuccess()

Then we should be good to go 🚀

@mtrezza
Copy link
Member

mtrezza commented May 3, 2022

It's out of the scope of parse-server to be able to emit/handle correctly these readiness probes, public traffic routing availability.

There are two sides to readiness:

  1. the sender (eg. load balancer) should not send until the recipient (Parse Server) is ready.

  2. the recipient (Parse Server) should not process any requests until it has finished its start-up procedure.

For (1) we already have the the /health endpoint, and it is definitely within Parse Server's (or the external express route's) scope that the endpoint handles requests that are sent before the server is ready, it should for example not crash or cause undefined behavior.

For (2) I don't think Parse Server should handle any type of request until is has started up, unless we want to implement a distinct "start-up" API, which would likely be a more complex discussion and I don't think is currently part of this discussion.

More generally, polling Parse Server for readiness is actually a somewhat outdated technique. The start-of-the-art mechanism would be to launch Parse Server with a configuration of hooks that are called once it's ready / failed / etc, and then the load balancer starts sending requests, restarts or replaces the instance. Also in this case, readiness management is very much within the scope of Parse Server.

@Moumouls
Copy link
Member

Moumouls commented May 8, 2022

Also in this case, readiness management is very much within the scope of Parse Server.

I think Parse Server should provide sufficient tools to handle a graceful start. But for example, PM2 can send traffic once it detects a server.listen(), K8 has a flexible readiness probe system, and in the case of parse-server, sending a query to /health will work perfectly. In Knative, Google cloud run, the traffic is also sent once the target port configured on GCP is listening.

I think the core problem is how to allow developers to use SDK/REST/GQL API in before/after migration and serverStartComplete function given that these functions should be called BEFORE listen to ensure a graceful start.

Or developers need to start a custom express app, with a dedicated port to handle on their side the graceful start with their related business logic ?

@dblythy dblythy requested a review from a team September 18, 2022 07:15
@dblythy
Copy link
Member Author

dblythy commented Sep 18, 2022

I'm not sure what's pending here or what the general feedback was. This approach covers the following objectives:

  • ES imports for cloud code as server won't be mounted to /parse before await .startApp() is called
  • Allows developers optional parameter to run custom initialization logic (such as building schema, custom database ops, or anything else) before mounting routes
  • Converts schema initialization to use direct methods (e.g data.create) rather than unnecessarily routing through express via the Parse JS SDK

@dblythy
Copy link
Member Author

dblythy commented Oct 12, 2022

Closing as this will be introduced as a breaking change in V6.

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.

Parse Server may expose HTTP routes before complete initialization
7 participants