-
Notifications
You must be signed in to change notification settings - Fork 310
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
[DISCUSS] Service Composition #178
Comments
I've created two branches with some basic prototyping in place. The existing tests pass when the 'default' invocation is used. service-composition-w-classes: converts the existing To launch a kernel server, you'd use: service-composition-w-options: introduces the option To launch a kernel server, you'd use: Based on these (quick) experiments, I think I'd lean toward using the options ( |
Thx @kevin-bates. As a end-user only looking at the CLI, I would favor the Scanning the diff, it seems like the Now to better understand your quote |
Hi @echarles - thanks for the comment/question. Yeah, I think My comment regarding One example came up just today in EG. The user would like to filter the list of running kernels by user. (They refer to /api/kernelspecs, but I'm fairly certain they're really talking about /api/kernels.) This kind of thing is really more about an extension on top of "kernel mode", but it I think it paints the picture. If "kernel mode" was implemented via subclassing, the subclass would have free reign to implement the desired endpoint - one that you wouldn't want to necessarily support/expose when running as a notebooks server. I suppose a couple things come to mind. Users could extend the So, at the end of the day 😄, I suppose I've just argued that either way doesn't make a huge difference. |
This is awesome, @kevin-bates. Thank you so much for writing out this proposal in a clear and thorough way. I may steal some of this text for documentation in the Jupyter Server. A lot of this harkens back to jupyter/enhancement-proposals#31. It's great to see how our conversation has evolved over time. Okay, here is a summary of my take: I suggest we go the subclass route; however, I might implement it differently than you have above. Instead of extending the Other thoughts:
Overall, I think the clear composability of the subclass design is more "pythonic" than the single entry-point + flag-mode switching. Yes, someone can introduce some back-door approach to composing classes with the single entry-point design, but IMO that breaks many of goals in the "Zen of Python" :)... |
Thanks for the response @Zsailer. Before I respond to your Mixin idea, I wanted to say that I agree with all your points on "Other thoughts". If folks wanted to create a ServerApp with none of the services available, who are we to say that shouldn't be possible? Regarding Mixins, I'm having trouble visualizing how this would work. I believe you're suggesting, based on the "other services" response, that every service listed in I'm also not following how the methods within the various Mixins would work themselves out. Would the mixin instance essentially be a container that exposes traits, managers and service names relative to itself and the handler and manager class implementations remain where they are? For example, if the main application wanted to get its list of services, it would call something like class ApiMixin():
def get_services():
return ['api'].extend(super(ApiMixin, self).get_services()) with def get_services():
return [] so that given the class definition of... class KernelServerApp(ApiMixin, AuthMixin, ConfigMixin, KernelsMixin, KernelspecsMixin, KernelResourceMixin, SecurityMixin, BaseServerApp): A call to Also, for the code in def init_configurables():
super(KernelServerApp, self).init_configurables() but could then go about using In cases where the current module just needs to know if a given mixin is "in play", would it just ask If what I've asked is your idea, this sounds interesting. I like the flexibility it introduces. On the other hand, it might be difficult for users to get the compositions correct. For example, they would need to know that KernelspecsMixin is required when KernelsMixin is selected. Some the content-related services would require that ContentsMixin be present, etc. Inter-mixin dependencies. 😄 But I suppose we could come up with some validation logic for these (few) circumstances. |
I've created branch service-composition-w-mixins to attempt to demonstrate ServerApp (and a KernelServerApp) implementation using mixins to express the composition. There were a few hurdles to overcome with this and I didn't like having to keep the class-based traits in the BaseServerApp class. I suspect there are ways to address this aspect of things but my traitlets fu just isn't strong enough. Also, I could see how the "goal" of encapsulation via mixins could get messy if there are ever circular dependencies that don't manifest otherwise. The issue there is that the order the mixin is listed in the class definition matters in some cases, and that might get confusing. I'm also seeing issues with aliases that I didn't deal with - especially after discovering those same issues exist in the |
Quick comment to get the ball rolling... options to overcome such dependency issues by order of complexity and gain:
|
In today's Server meeting, we discussed service composition or the ability to optionally specify which services should be exposed by a given instance of Jupyter Server. This issue is intended to summarize that discussion and, more importantly, stimulate discussion amongst the interested community.
TL;DR
The purpose of this issue is to discuss how we might adjust server internals to enable composability of services. This is more than just exposing a list-based trait consisting of which services should be exposed - but more in line with an intuitive grouping of related functionality. Regardless of the outcome of this discussion, the default behavior will be that all services are exposed so as to best preserve existing functionality.
Introduction
One of the goals of Jupyter Server is to enable the ability for users to configure subsets of services. For example, if a user wanted to configure the server to only serve kernels, they should be able to configure only the necessary services for exposing kernel functionality. Similarly, one should be able to configure a server as a “content server” where things like kernel functionality is not present.
Within the main server application, there is a general assumption that all services are enabled and available. Applications that wish to alter the set of “system-defined” services must subclass ServerApp and override the set of services. Still, there are general assumptions throughout the ServerApp class instance that assumes all services are present. This issue is meant to generate discussion of how we might want to refactor service-relative functionality so that the Server itself can be more easily configured into “units of functionality”. Please note, however, that regardless of where this discussion leads, the default behavior of Jupyter Server will be, generally speaking, that of today’s Notebook server in that, by default, all services (or “units of functionality”) will be present and enabled.
Terminology
Service: By service I mean the portion of the functionality that is exposed via a REST API. This does not include underlying “manager” instances. In many cases, we still want to use the “manager” corresponding to a given functional unit (e.g., FileContentsManager), yet not expose that functionality via a Service. That said, exposure via a service should imply the existence of an underlying “manager” instance (just not vice versa).
Extension vs. Subclass: It’s easy to view a subclass as an extension of the its superclass - that’s correct. But for the purposes of this discussion, we should treat the term extension as meaning the implementation of a server extension in the sense that the functionality it provides is in addition to the functionality provided by the server. Whereas subclassing implies that the existing functionality provided by the server is altered or transformed.
Default services
The set of default services can be found here. For simplicity, I’ve added them below:
The idea, relative to this proposal, is that all these services would be exposed by default. However, from a composability perspective, we should discuss which of these services are required and which are optional and how composition would be expressed.
Required services
By definition, required services are those services that need to be present when any optional services are configured. I think we'd want to enforce behavior such that the server cannot be started with JUST required services configured. That said, we shouldn't preclude a base server (consisting of only required services), in conjunction with at least one server extension that introduces its own service, from being started. As a result, the complete picture should be considered prior to automatically shutting down an "invalid server" (i.e. one consisting of only required services).
Of the services listed above, I think only 'auth', 'security' and perhaps 'config' and 'api' would be considered "required". 'config' is in this group because anything pertaining to a "valid" server will have configuration. 'api' is in this group because there needs to be some description for how the REST APIs of the "valid" server are to be used. However, the handler for this service would likely build its response based on what other (optional) services are configured. This implies today's single yaml file would be broken down to the composable service boundaries, probably even with separate 'parameters', 'paths' and 'definitions' sections for each, so as to enable easier composition of the response.
Optional services and functional composition
Obviously, services not considered required services, would fill out the set of optional services. However, we probably shouldn't expose these services individually, but, instead, as units of logical functionality. For example, users wishing to run Jupyter Server as a Kernel Server would need 'kernels', 'kernelspecs' and (not listed) 'kernelspec_resources' (for serving resource files). One approach would be to group these into a
kernel_services
set. Likewise, users wishing to run Jupyter Server as a Content Server would need 'contents', 'edit', 'files', 'view', so these would be grouped into a 'content_services' set, etc.Note that 'sessions' and 'nbconvert' weren't in either
kernel_services
orcontent_services
, yet are required by today's Notebook front-ends. For these we could do nothing (since the default is everything) or create anotebook_services
set that would be composed ofkernel_services
,content_services
,'sessions'
and'nbconvert'
(we'd probably add 'terminal' to this as well, but that's already optional by virtue of the existence of terminado- although we should make that option more explicit).One could then envision command-line options of
jupyter server --kernel-services
orjupyter server --content-services
orjupyter server --notebook-services
(which is the same as justjupyter server
) where each of these "service-oriented" options are mutually exclusive. Or functionality-based sub-commands could be implemented enabling commands likejupyter kernel-server
andjupyter content-server
.Other services
In looking through the code, there are a number of endpoints that are not addressed in the
default_services
set. Things like 'bundler', 'terminal', and 'metrics' (prometheus) to name a few should be considered for placement, perhaps even in the required set (metrics seems logical for example).General refactoring
One of the areas in which this issue came up is during server startup and the messages it produces...
If we make services functionally composable, we will need to address the messaging produced during startup. For example, if I don't run with contents services enable, then I shouldn't see the message about where notebook files are being served from. Likewise, if I'm not using kernels, then messages regarding kernels should not be displayed, etc. As a result, if we were to adopt the sub-command approach (not sure that's the correct term) then we could subclass
ServerApp
(or some BaseServerApp class) with classes likeKernelServer
andContentsServer
where these "apps" simply provide the set of services to its superclass. Applications could then extend those subclasses, etc. For example, with this approach, each subclass would implement its ownrunning_server_info()
method since each knows its set of functionality.Inter-service dependencies
Some services may have dependencies on others. However, it's not clear to me if those dependencies actually consist of other services or are confined to just the managers from those services. For example, 'sessions' uses managers from both Kernels and Contents, but doesn't hit the endpoints exposed by those services. As such, those kinds of dependencies should be fine. If there are instances of one service (or its manager) hitting endpoints of another service, then when we form the functional grouping for that service, it needs to include the other service(s).
There's probably more to say about all this, but this seems like a good place to stop.
Comments, suggestions, concerns are welcome (please).
The text was updated successfully, but these errors were encountered: