-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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
[RFC] Allow 'leasing' of Expression constructs #105164
[RFC] Allow 'leasing' of Expression constructs #105164
Conversation
Pinging @elastic/kibana-presentation (Team:Presentation) |
Pinging @elastic/kibana-app-services (Team:AppServices) |
26dcaf2
to
545b15e
Compare
@elasticmachine merge upstream |
💚 Build SucceededMetrics [docs]Public APIs missing comments
Async chunks
Page load bundle
History
To update your PR or re-run it, just comment with: |
When I read through the problems listed and try to distil them, I think this boils down to a page load bundle size issue only. If the page bundle size was not affected at all, do any problems remain?
If this didn't affect page load time, I would consider this a feature, not a bug. We don't yet expose the expression language in other applications, but it's been discussed many times, and if we had that functionality, I would consider it a bad UX if a user couldn't copy an expression string from Canvas to Lens or to Dashboard, and have it work.
Taking a hypothetical example, it sounds like we have a Assuming this had no affect on page size, why is it problematic that
If you are talking about plugins adding registry implementations after the setup lifecycle, I consider this an anti-pattern, and something that should be solved with #105439. It sounds there like again, the only issue is page bundle size.
Yes, but I don't know if this is something we can solve. Even if we had recommendations, I don't think we can control what PluginB, in the example above, chooses to expose. If I try to condense those problems down, I come up with: Can we improve our registry pattern to avoid increasing the page load bundle size significantly? Some problems that can cause this:
Canvas registers many ExpressionFunctions that aren't used by end users outside of Canvas (yet), because we don't expose the raw expression string anywhere else in the UI. However, we've talked about it a lot, and, a developer might want to use those functions. If I understand correctly, a developer using an expression function is what caused this issue to be raised in the first place. IMO, an ideal solution is something that helps us avoid page bloat for all types of registries, but allows the implementations in them to still be globally accessible, and available to all plugins, at all times (predictability). I like the idea of only loading the implementations when they are needed, but it sounds like that might not solve the page bundle size, according to your experiment Clint. Maybe this is worth digging into a bit more (cc @lukeelmers ) I think allowing expression functions to be registered and unregistered at any time has the potential to cause a lot of issues. For example, while you are intending to use this new functionality in |
From the beginning, the whole promise of Expressions has been that they could eventually be used anywhere in Kibana. So I share Stacey's concern about allowing expression functions to be registered/unregistered at any time. We should be clear about which functions are public and which are private (which I believe we have a solution for -- my thoughts on that below), and then should be very careful about breaking the public ones.
I understand the concern here and how implicit dependencies on functions can cause confusion, however I am not sure how this isn't already solved by the existing kibana/src/plugins/expressions/common/service/expressions_services.ts Lines 154 to 163 in 0ef3cbe
In other words, a plugin can:
Does this not solve the problem you are describing above? The "fork" and "no-fork" approaches are not mutually exclusive: you can use both to control sharing of the expression functions.
++ Agree on this point, but will add my thoughts to that thread. Performing
I know I've said this before, but I think designing more registries to be async could solve a lot of these problems. Then for situations like Canvas, where a bunch of functions are registered during |
Thanks for this so far. Let's see if I can address all of the concerns... to be clear, I'm not proposing this PR as the solution, but rather demonstrating the need/a way to sort it.
Expressions, but not all Expression Functions. We've gone to great lengths to restrict access to code that is not intended to be shared, and yet we're saying that anything registered at run time should be globally accessible. Canvas has no dependents, and yet we're forcing Canvas to share its code with the rest of Kibana simply by utilizing a common service... and forcing the Kibana application to load all of the Canvas code up-front... even if the user has no interest in loading Canvas. Canvas is currently discussing how to handle deprecations with #101377 ... Expressions, especially those that render output, are incredibly if not impossible to migrate automatically. We're already concerned about how to handle this with end-users in the Canvas UI, but to be concerned about where else in Kibana our functions may be consumed at run time is an enormous burden to bear.
Yes and no. We're basically saying the same thing, but in different ways. I'm arguing that up-front loading of code is inherently wasteful, but also that the sharing of that code must be intentional. In my PR before, an example plugin broke because it was relying on a Canvas expression we had zero intention of being used anywhere else... Canvas has "zero dependents" currently, remember. I don't think @wylieconlon or @flash1293 have any expectation (or intention to support) that Lens Expressions "should just render" in Canvas outside of an embeddable. They certainly don't "just work" that way today.
Canvas does fork the Expressions service, but my understanding from you and @ppisljar and @streamich was that we wanted forks of the service to stop. Canvas can certainly fork the service to keep our code private. But if Lens were to do that, would their embeddables work?
I'm not convinced that's the only issue here. It's a big one, to be sure. I don't think we should be loading so much code up front for Canvas, and I can get that as small as I can (#104990). Even if we fork and separate the definitions from the implementations, we're still loading code other plugins won't use. That feels incorrect to me. But I'm also concerned about the implicit vs. explicit sharing of code. We literally block people from importing the source code unless we add it to I'm all for forking the Expressions Service and explicitly sharing functions, but I think it's heavy handed... I'm not sure though, at this point. |
Yea, this is a good point, and a bug, imo. That example plugin should have listed Canvas as a dependency if it required the specific markdown function to be available. I've discussed this tripping point with @ppisljar before, which is touched on in this closed issue. We talked about discouraging access like However, that's just a recommendation, and it'd still be possible for a developer to use the generic registry and not add a dependency. I suspect this is even more difficult to solve with expressions because it's a string. At least with the embeddable example, the plugin would probably import For example, what happens if a developer hard codes an expression string and uses a certain expression function they know is registered by
I'm still trying to understand the root problem here. Can we try the "5 whys" approach? Why does it feel incorrect? What is problematic? I'm not being facetious, I'm really trying to understand. Is it a problem that it's shared, or is it a problem that it's not obvious it's shared? Is the main issue migrations/BWC? |
i need to dig a bit deeper but here are some initial thoughts i have: it seems we have two problems listed above:
I am gonna list some of the problems we know of with forking, and i think they all apply to suggested solution as well:
we do have functions in canvas referencing other saved objects ... all panels using them will all break with 8.0 unless we fix this
apps using fork will not be able to support backgrounding search sessions, alerting, reporting and won't be able to properly support migrations and saved object reference extraction/injection. unless we find a solution for private functions that would not break any of the above i would not be going forward with it. |
A Quick Comment
I think that's the disconnect here, @stacey-gammon: that example plugin didn't have a dependency on Canvas. It didn't import any of its code, require any APIs or anything else. The Even if we look past that, unless the person writing the plugin and making the default of that textbox specifically knew the But worst of all: Canvas didn't know The anti-pattern in this case is unknowingly adding functionality considered internal to a global state to be used anywhere else. And this is why we need to fix the bug we have in Canvas, (see below). But we also need to start organizing our truly common Expression constructs into packages or the Expressions Plugin itself. Closing the RFCBased on the comments here, and conversations elsewhere, I'm going to abandon this PR/RFC in favor of work proposed here: While a Nice Try™️ at keeping Canvas from polluting the Expressions plugin with its functions, I've come to believe that we need a much more holistic and philosophical solution to how Expression constructs are registered in (and persisted throughout) Kibana. Canvas will sort the plugin lifecycle as defined in the issue above, but also fix a bug where Expression constructs are registered twice: once with its fork and once with the global plugin. In addition, I'm volunteering myself to prep a "history of expressions and their use" to guide conversations in the Expressions Working Group, (cc: @petrklapka) I think we really need to revisit a few topics:
Thanks for the feedback here, all. While we still have some of the same issues we had when I started this RFC, the good news is those conversations have started. |
We can't prevent a plugin from storing and exposing state/functionality passed to it via a lifecycle contract method. I think our stance should be:
|
RFC - Allow "Leasing" of Expression Constructs
Related: #104266
Related: #105439
Summary
This PR enhances the Expressions
executor
and plugin-provided service to allow a consumer to register functions, types and renderers on a temporary basis-- that I've called 'leasing'-- returning a single callback to remove them from the runtime at an appropriate time, (e.g. lease on pluginmount
, remove on pluginunmount
). This prevents unexpected leaking of expression functions to other plugins that were not intended or expected.Problem
Currently, a plugin registers its Expression constructs (renderers, functions, types) during plugin
setup
, (or is intended to). This poses a number of difficulties:private
flag, namespacing, forking, etc. None have been implemented.public
orcommon
folder) are still shared implicitly.setup
orstart
of a plugin, the constructs-- which may be few, like Lens, or many, like Canvas-- are included in the page bundle... regardless of if that construct is actually used in contexts beyond that plugin.mount
or by other asynchronous means, once the plugin is loaded, the constructs have still "polluted" the global context.All of this makes passivity impossible. Expression-based constructs, once registered, become global, period.
Example
As an example, see the change that became necessary in #104264 when Canvas moved its function registration to plugin
mount
rather thansetup
: the Expressions Explorer example had been using an implicit expression,markdown
, rather than the explicitly-shared expression,markdown_vis
. Notice that the Canvasmarkdown
function is not even in thepublic
directory. This is a perfect example of the "contract leak" the expressions registration dilemma causes.While only in an example app, the consequence is clear: changing the
markdown
expression in Canvas, regardless of how minor or measured the change from a Canvas perspective, would have broken this plugin.Proposal
In #104264, Canvas moved its registration to the mount of the plugin, but they still pollute the Expressions plugin, (they are unavailable on mount, but are available globally on unmount).
This PR evens this out by providing new methods (and philosophy) that keeps plugins from polluting the Expressions plugin, while still giving a means to register their constructs when the plugin is active.
New Methods
New Philosophy
setup
portion of their plugin.common
orpublic
(as appropriate), or in a shared package. They should avoid registering them themselves in the global context.mount
and remove them onunmount
.Impact
This change should be passive to most plugins, but constructs that were registered in Canvas are already not available as of #104264. Other plugins can simply "opt-in" as they see fit.
We should also begin to create shared packages of expressions we view as "common" or "global" as soon as possible.
Making this change and establishing these standards now, before more expressions or expressions-consuming solutions are written, would benefit the entire ecosystem moving forward. This change is also passive in the sense no one is required to do anything different... but continuing to "pollute" the executor carries a known risk, (with a mitigation).
Request for Comment
I'm seeking general feedback, but also the following specific feedback:
register
andlease
? In other words, should we changeregister
to return a callback that is simply ignored by the consumer, or is the syntactic sugar oflease
more explicit and useful?register()
andlease()
which takes multiple types of construct?lease( { renderers: ..., types: ...} )
orlease([...stuff])
and use a TS type-guard?