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

Runtime hook for server startup/init event #1987

Closed
1 task
septatrix opened this issue Dec 6, 2023 · 8 comments
Closed
1 task

Runtime hook for server startup/init event #1987

septatrix opened this issue Dec 6, 2023 · 8 comments

Comments

@septatrix
Copy link
Contributor

septatrix commented Dec 6, 2023

Describe the feature

It would be great if there were a possibility to run some code once during server startup. My potential use case would be running migrations programmatically. Currently there does not exist a hook for this.

It is unclear if running such code inside the defineNitroPlugin satisfies these requirements (run only once, run before any request is handled) and if yes, that should likely be documented.

PS: However, even if they do meet these requirements it does not seem like they support async calls.

// is this guaranteed to run once, and before any request?
// PS: is async allowed here?
export default defineNitroPlugin(async () => await dbsession.migrate())

See also nuxt/nuxt#22486

Additional information

  • Would you be willing to help implement this feature?
@botic
Copy link

botic commented Dec 22, 2023

A special hook running once at start up would be great to e.g. log some debugging information into the log stream of a Nuxt container. Imagine something like this:

export default defineNitroPlugin((nitroApp) => {
  console.log('Nitro plugin', nitroApp);

  // Variant a) make config accessible via nitroApp
  console.log(`Using Backend API Version: ${nitroApp.runtimeConfig.apiVersion}`);    
  console.log(`Feature Flag A: ${nitroApp. runtimeConfig.public.featureA}`);    

  // Variant b) start hook
  nitroApp.hooks.hook("start", async (config) => {
      console.log(`Using Backend API Version: ${runtimeConfig.apiVersion}`);    
      console.log(`Feature Flag A: ${runtimeConfig.public.featureA}`);    
  });
});

This would make it much easier to write out hints e.g. for a helpdesk team. They could just start with the log and look for the important configuration options.

Btw. you already use the config in createNitroApp(), but it's never exposed to plugins.

@pi0
Copy link
Member

pi0 commented Dec 23, 2023

@botic nitro plugins actually run once during server startup. what is current issue with Varaint a? (feel free to share feedback in a new issue if have issues or questions about it not working)


Example:

export default defineNitroPlugin(() => {
  const runtimeConfig = useRuntimeConfig();
  console.log(
    [
      `Using Backend API Version: ${runtimeConfig.apiVersion}`,
      `Feature Flag A: ${runtimeConfig.public.featureA}`,
    ].join('\n')
  );
});

https://stackblitz.com/edit/github-67l6eo?file=plugins%2Fbanner.ts,nitro.config.ts

@pi0
Copy link
Member

pi0 commented Dec 23, 2023

@septatrix

It is unclear if running such code inside the defineNitroPlugin satisfies these requirements (run only once, run before any request is handled) and if yes, that should likely be documented.

Yes, this is the behavior. Nitro plugins run once during server startup. It is documented but PR welcome to improve and reword

PS: However, even if they do meet these requirements it does not seem like they support async calls.

You can track support for async plugins in #915 however generally I wouldn't recommend blocking (entire) server for tasks such as DB migrations. Instead you could create a utility like ensureDBInitialized in utils/ and use it in the routes that need it or use a middleware.

Nitro Tasks API is also a relevant topic you might like to track.

@septatrix
Copy link
Contributor Author

Nitro plugins run once during server startup. It is documented but PR welcome to improve and reword

I must have missed the "on the first" in the docs (- why does it even say "first", are there cases in which multiple initializations are possible?).

PS: However, even if they do meet these requirements it does not seem like they support async calls.

You can track support for async plugins in #915...

Thanks for the pointer, async plugins would be perfect for me than.

...however generally I wouldn't recommend blocking (entire) server for tasks such as DB migrations. Instead you could create a utility like ensureDBInitialized in utils/ and use it in the routes that need it or use a middleware.

Nitro Tasks API is also a relevant topic you might like to track.

While performing migrations on startup might not sound ideal, it can often be the most practical solution and is widely used due to this. While it might not work for distributed stuff with all its complications, for single node services it is perfect. There is always a small downtime when restarting to the newer version, a few more seconds won't hurt. It is a lot easier to apply, instead of e.g. having to run commands inside a docker container.
And the overhead of having an ensureMigrated middleware is also not always wanted. I would have to somehow know which DB schema is currently deployed which either requires a DB read or file read, both of which would also be any and would then happen for every route which needs the DB (unless I start introducing global state within Nitro which one should also try to prevent). So for me running migrations on startup is the most practical solution.

@pi0
Copy link
Member

pi0 commented Dec 23, 2023

Async plugins, if/when supported will likely show a warning if their evaluation takes too long which can happen for dbs.

Also for Nitro's built-in database API, lazy init will be used because you need the database once you need it(at least IMO). This kind of lazy startup is one of the core principles in Nitro's server design that ensures it is fast and scalable.

However, I respect your opinion about the convenience of blocking the whole server startup also it is nothing strange indeed many (traditional) approaches of DB migration block server startup.

I made an alternative discussion to support init: and startup: tasks we can manage them better at least with the same possibility of async plugins. So let's track it there. (~> #2021)

@pi0 pi0 closed this as not planned Won't fix, can't repro, duplicate, stale Dec 23, 2023
@dervondenbergen
Copy link
Contributor

Hello @pi0, colleague of @botic here.

Thanks for the information that Nitro Plugins only run once, we somehow missed this in the docs which you linked.

We use Nitro in the context of Nuxt, and tried our usecase there using all kind of hooks, both defined in the nuxt.config.ts and in a defineNuxtPlugin/defineNitroPlugin.

Not defining a hook and logging directly in the defineNitroPlugin callback as a server plugin finally worked 🎉

Bildschirmfoto 2023-12-23 um 16 41 14

@botic
Copy link

botic commented Dec 23, 2023

@botic nitro plugins actually run once during server startup. what is current issue with Varaint a? (feel free to share feedback in a new issue if have issues or questions about it not working)

Yes, Variant A is perfect. I cannot reproduce my issue not having access to useRuntimeConfig() but since @dervondenbergen got it working, I'm totally fine 👌

I personally was looking for an init or start hook at the beginning. My first thought was: If there is a close hook, where is its counterpart? But variant A is just fine and maybe just needs a hint / example in the documentation.

@septatrix
Copy link
Contributor Author

Also for Nitro's built-in database API, lazy init will be used because you need the database once you need it(at least IMO). This kind of lazy startup is one of the core principles in Nitro's server design that ensures it is fast and scalable.

How would such a lazy init work? The current nitro docs do not mention anything about database migrations and the usage example always calls DROP TABLE IF EXISTS users which is a no go. My initial guess was that it could be handled by the useDatabase() composable but that is synchronous so also likely not where such a lazy init would be applied. My final guess would be to always perform migrations before each await db.sql`SELECT ...`; but that also is dump because that would add at least a round trip to each database call because migration status would need to be checked.

My assumption is that this is not implemented, yet, however I am curious how you intend to integrate it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants