Description
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:
# A list of services whose handlers will be exposed.
# Subclasses can override this list to
# expose a subset of these handlers.
default_services = (
'api',
'auth',
'config',
'contents',
'edit',
'files',
'kernels',
'kernelspecs',
'nbconvert',
'security',
'sessions',
'shutdown',
'view'
)
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
or content_services
, yet are required by today's Notebook front-ends. For these we could do nothing (since the default is everything) or create a notebook_services
set that would be composed of kernel_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
or jupyter server --content-services
or jupyter server --notebook-services
(which is the same as just jupyter server
) where each of these "service-oriented" options are mutually exclusive. Or functionality-based sub-commands could be implemented enabling commands like jupyter kernel-server
and jupyter 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...
[I 12:34:58.097 ServerApp] Serving notebooks from local directory: /Users/kbates/repos/oss/jupyter/notebook/dist
[I 12:34:58.097 ServerApp] Jupyter Server 0.2.0.dev0 is running at:
[I 12:34:58.097 ServerApp] http://localhost:8888/?token=c617532e143f1dba866ecc316e79eb0ed9dc52b501003cfe
[I 12:34:58.097 ServerApp] or http://127.0.0.1:8888/?token=c617532e143f1dba866ecc316e79eb0ed9dc52b501003cfe
[I 12:34:58.097 ServerApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
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 like KernelServer
and ContentsServer
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 own running_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).