-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
add synchronous config access API #88981
Changes from 7 commits
d7bd50e
e46be42
8fbb27b
d4083bd
5a6a761
2c7579e
1a5a47e
cafc7a6
b833984
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; | |
import { Type } from '@kbn/config-schema'; | ||
import { isEqual } from 'lodash'; | ||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; | ||
import { distinctUntilChanged, first, map, shareReplay, take } from 'rxjs/operators'; | ||
import { distinctUntilChanged, first, map, shareReplay, take, tap } from 'rxjs/operators'; | ||
import { Logger, LoggerFactory } from '@kbn/logging'; | ||
|
||
import { Config, ConfigPath, Env } from '.'; | ||
|
@@ -32,13 +32,15 @@ export class ConfigService { | |
private readonly log: Logger; | ||
private readonly deprecationLog: Logger; | ||
|
||
private validated = false; | ||
private readonly config$: Observable<Config>; | ||
private lastConfig?: Config; | ||
|
||
/** | ||
* Whenever a config if read at a path, we mark that path as 'handled'. We can | ||
* then list all unhandled config paths when the startup process is completed. | ||
*/ | ||
private readonly handledPaths: ConfigPath[] = []; | ||
private readonly handledPaths: Set<ConfigPath> = new Set(); | ||
private readonly schemas = new Map<string, Type<unknown>>(); | ||
private readonly deprecations = new BehaviorSubject<ConfigDeprecationWithContext[]>([]); | ||
|
||
|
@@ -55,14 +57,17 @@ export class ConfigService { | |
const migrated = applyDeprecations(rawConfig, deprecations); | ||
return new LegacyObjectToConfigAdapter(migrated); | ||
}), | ||
tap((config) => { | ||
this.lastConfig = config; | ||
}), | ||
shareReplay(1) | ||
); | ||
} | ||
|
||
/** | ||
* Set config schema for a path and performs its validation | ||
*/ | ||
public async setSchema(path: ConfigPath, schema: Type<unknown>) { | ||
public setSchema(path: ConfigPath, schema: Type<unknown>) { | ||
Comment on lines
-65
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Method was |
||
const namespace = pathToString(path); | ||
if (this.schemas.has(namespace)) { | ||
throw new Error(`Validation schema for [${path}] was already registered.`); | ||
|
@@ -94,43 +99,43 @@ export class ConfigService { | |
public async validate() { | ||
const namespaces = [...this.schemas.keys()]; | ||
for (let i = 0; i < namespaces.length; i++) { | ||
await this.validateConfigAtPath(namespaces[i]).pipe(first()).toPromise(); | ||
await this.getValidatedConfigAtPath$(namespaces[i]).pipe(first()).toPromise(); | ||
} | ||
|
||
await this.logDeprecation(); | ||
this.validated = true; | ||
} | ||
|
||
/** | ||
* Returns the full config object observable. This is not intended for | ||
* "normal use", but for features that _need_ access to the full object. | ||
* "normal use", but for internal features that _need_ access to the full object. | ||
*/ | ||
public getConfig$() { | ||
return this.config$; | ||
} | ||
|
||
/** | ||
* Reads the subset of the config at the specified `path` and validates it | ||
* against the static `schema` on the given `ConfigClass`. | ||
* against its registered schema. | ||
* | ||
* @param path - The path to the desired subset of the config. | ||
*/ | ||
public atPath<TSchema>(path: ConfigPath) { | ||
return this.validateConfigAtPath(path) as Observable<TSchema>; | ||
return this.getValidatedConfigAtPath$(path) as Observable<TSchema>; | ||
} | ||
|
||
/** | ||
* Same as `atPath`, but returns `undefined` if there is no config at the | ||
* specified path. | ||
* Similar to {@link atPath}, but return the last emitted value synchronously instead of an | ||
* observable. | ||
* | ||
* {@link ConfigService.atPath} | ||
* @param path - The path to the desired subset of the config. | ||
*/ | ||
public optionalAtPath<TSchema>(path: ConfigPath) { | ||
return this.getDistinctConfig(path).pipe( | ||
map((config) => { | ||
if (config === undefined) return undefined; | ||
return this.validateAtPath(path, config) as TSchema; | ||
}) | ||
); | ||
public atPathSync<TSchema>(path: ConfigPath) { | ||
if (!this.validated) { | ||
throw new Error('`atPathSync` called before config was validated'); | ||
} | ||
const configAtPath = this.lastConfig!.get(path); | ||
return this.validateAtPath(path, configAtPath) as TSchema; | ||
Comment on lines
+133
to
+138
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. During testing, directly calling |
||
} | ||
|
||
public async isEnabledAtPath(path: ConfigPath) { | ||
|
@@ -144,10 +149,7 @@ export class ConfigService { | |
const config = await this.config$.pipe(first()).toPromise(); | ||
|
||
// if plugin hasn't got a config schema, we try to read "enabled" directly | ||
const isEnabled = | ||
validatedConfig && validatedConfig.enabled !== undefined | ||
? validatedConfig.enabled | ||
: config.get(enabledPath); | ||
const isEnabled = validatedConfig?.enabled ?? config.get(enabledPath); | ||
|
||
// not declared. consider that plugin is enabled by default | ||
if (isEnabled === undefined) { | ||
|
@@ -170,15 +172,13 @@ export class ConfigService { | |
|
||
public async getUnusedPaths() { | ||
const config = await this.config$.pipe(first()).toPromise(); | ||
const handledPaths = this.handledPaths.map(pathToString); | ||
|
||
const handledPaths = [...this.handledPaths.values()].map(pathToString); | ||
return config.getFlattenedPaths().filter((path) => !isPathHandled(path, handledPaths)); | ||
} | ||
|
||
public async getUsedPaths() { | ||
const config = await this.config$.pipe(first()).toPromise(); | ||
const handledPaths = this.handledPaths.map(pathToString); | ||
|
||
const handledPaths = [...this.handledPaths.values()].map(pathToString); | ||
return config.getFlattenedPaths().filter((path) => isPathHandled(path, handledPaths)); | ||
} | ||
|
||
|
@@ -210,22 +210,17 @@ export class ConfigService { | |
); | ||
} | ||
|
||
private validateConfigAtPath(path: ConfigPath) { | ||
return this.getDistinctConfig(path).pipe(map((config) => this.validateAtPath(path, config))); | ||
} | ||
|
||
private getDistinctConfig(path: ConfigPath) { | ||
this.markAsHandled(path); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We were flagging configurations as handled every time we called The only calls to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we move There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validating the config within the whole config obs
May be tricky because we would need to already prepare each child observable here, one per schema (we don't want In a similar way, ideally, Also, as you said on slack, the timing between emissions and schema registration needs to be taken into account here. When registering a schema, we need to validate the associated part of the config.
I agree that this is still a performance improvement though, I will create an issue. |
||
|
||
private getValidatedConfigAtPath$(path: ConfigPath) { | ||
return this.config$.pipe( | ||
map((config) => config.get(path)), | ||
distinctUntilChanged(isEqual) | ||
distinctUntilChanged(isEqual), | ||
map((config) => this.validateAtPath(path, config)) | ||
); | ||
} | ||
|
||
private markAsHandled(path: ConfigPath) { | ||
this.log.debug(`Marking config path as handled: ${path}`); | ||
this.handledPaths.push(path); | ||
this.handledPaths.add(path); | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We were using an array instead of a set to store the used paths, meaning that we were eventually having duplicates. (Impact was minimal as we are using a shareReplay in the plugin context's
config.create
observable, but still)