perf(auth): lazily load auth drivers to reduce memory usage - #28146
perf(auth): lazily load auth drivers to reduce memory usage#28146Vansh1811 wants to merge 6 commits into
Conversation
Refactor auth provider registration to use async/await for dynamic imports and provider instance creAuth drivers for LDAP, OAuth2, OpenID, and SAML pull in heavyweight dependencies (ldapjs, openid-client, @node-saml/node-saml, etc.) that were previously imported eagerly at the top of api/src/auth.ts. This meant every one of these packages was loaded into memory on every server boot, even when only the local provider was configured. This change replaces the static imports with dynamic import() calls inside getProviderInstance, so only the drivers that are actually referenced by AUTH_PROVIDERS get loaded. Fixes directus#24334
Reduced memory usage on startup by lazily loading LDAP, OAuth2, OpenID, and SAML auth drivers via dynamic imports, ensuring dependencies are loaded only when configured via AUTH_PROVIDERS.
Add tests for dynamic loading of authentication providers.
ComfortablyCoding
left a comment
There was a problem hiding this comment.
Thanks for the PR! Please see my comments below.
In addition, please ensure the PR template is followed. As this is a performance-related change, please provide stats demonstrating that it provides a meaningful improvement.
| /** | ||
| * Lazily imports and instantiates the auth driver matching the given type. | ||
| * | ||
| * Auth drivers (LDAP, OAuth2, OpenID, SAML) pull in heavyweight dependencies | ||
| * (ldapjs, openid-client, etc). Dynamically importing them here ensures that | ||
| * only the drivers actually configured via AUTH_PROVIDERS get loaded into | ||
| * memory, instead of all drivers being loaded eagerly on every boot. | ||
| */ |
There was a problem hiding this comment.
Let's simplify this
| /** | |
| * Lazily imports and instantiates the auth driver matching the given type. | |
| * | |
| * Auth drivers (LDAP, OAuth2, OpenID, SAML) pull in heavyweight dependencies | |
| * (ldapjs, openid-client, etc). Dynamically importing them here ensures that | |
| * only the drivers actually configured via AUTH_PROVIDERS get loaded into | |
| * memory, instead of all drivers being loaded eagerly on every boot. | |
| */ | |
| /** | |
| * Lazily loads the auth driver for the given type. | |
| */ |
| import { | ||
| LDAPAuthDriver, | ||
| LocalAuthDriver, | ||
| OAuth2AuthDriver, | ||
| OpenIDAuthDriver, | ||
| SAMLAuthDriver, | ||
| } from './auth/drivers/index.js'; |
There was a problem hiding this comment.
While this handles lazy instantiation, the drivers are still statically imported in the controller
directus/api/src/controllers/auth.ts
Line 7 in e29e467
|
|
||
| // Always register default provider | ||
| const defaultProvider = getProviderInstance('local', options)!; | ||
| const defaultProvider = await getProviderInstance('local', options)!; |
There was a problem hiding this comment.
The ! is asserting the promise, not the value, is still seen as possibly undefined by TS
| expect(samlDriverCtor).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| test('registerAuthProviders dynamically imports the oauth2 driver when configured', async () => { |
There was a problem hiding this comment.
The first test, this one and the next 2 essentially all test the same flow, let's condense them down to one.
| test('registerAuthProviders only dynamically imports drivers that are configured', async () => { | ||
| mockEnv['AUTH_PROVIDERS'] = 'ldap_provider'; | ||
|
|
||
| const { registerAuthProviders, getAuthProvider } = await import('./auth.js'); | ||
|
|
||
| await registerAuthProviders(); | ||
|
|
||
| expect(ldapDriverCtor).toHaveBeenCalledTimes(1); | ||
| expect(oauth2DriverCtor).not.toHaveBeenCalled(); | ||
| expect(openidDriverCtor).not.toHaveBeenCalled(); | ||
| expect(samlDriverCtor).not.toHaveBeenCalled(); | ||
| expect(getAuthProvider('ldap_provider')).toEqual({ type: 'ldap' }); | ||
| expect(getAuthProvider('default')).toEqual({ type: 'local' }); | ||
| }); | ||
|
|
||
| test('registerAuthProviders does not import unused drivers when AUTH_PROVIDERS is unset', async () => { | ||
| const { registerAuthProviders } = await import('./auth.js'); | ||
|
|
||
| await registerAuthProviders(); | ||
|
|
||
| expect(ldapDriverCtor).not.toHaveBeenCalled(); | ||
| expect(oauth2DriverCtor).not.toHaveBeenCalled(); | ||
| expect(openidDriverCtor).not.toHaveBeenCalled(); | ||
| expect(samlDriverCtor).not.toHaveBeenCalled(); | ||
| }); |
There was a problem hiding this comment.
These (and subsequent) tests do not assert lazy loading is taking place, only that the constructor is not instantiating. Any possible regression of the lazy load will therefor not be caught.
|
|
||
| // Register configured providers | ||
| providerNames.forEach((name: string) => { | ||
| for (let name of providerNames) { |
There was a problem hiding this comment.
This will load them sequentially, no need in this case, let's load them in parallel.
Auth drivers for LDAP, OAuth2, OpenID, and SAML pull in heavyweight dependencies (ldapjs, openid-client, @node-saml/node-saml, etc.) that were previously imported eagerly at the top of api/src/auth.ts. This meant every one of these packages was loaded into memory on every server boot, even when only the local provider was configured.
This change replaces the static imports with dynamic import() calls inside getProviderInstance, so only the drivers that are actually referenced by AUTH_PROVIDERS get loaded.
Fixes #24334
What's Changed
Tested Scenarios