Skip to content

Commit a710653

Browse files
authored
Add optional user to cluster options handler (dask#329)
The `handler` function provided to the `Options` object for `cluster_options` can now take an optional `user` argument. This can be used to provide user-specific configuration, without requiring stashing the `user` in a closure from `cluster_options`.
1 parent e46148c commit a710653

File tree

4 files changed

+81
-18
lines changed

4 files changed

+81
-18
lines changed

dask-gateway-server/dask_gateway_server/backends/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ async def process_cluster_options(self, user, request):
9696
try:
9797
cluster_options = await self.get_cluster_options(user)
9898
requested_options = cluster_options.parse_options(request)
99-
overrides = cluster_options.get_configuration(requested_options)
99+
overrides = cluster_options.get_configuration(requested_options, user)
100100
config = self.cluster_config_class(parent=self, **overrides)
101101
except asyncio.CancelledError:
102102
raise

dask-gateway-server/dask_gateway_server/options.py

+27-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import copy
2+
import inspect
23
import textwrap
34
from collections import OrderedDict
45
from collections.abc import Sequence
@@ -17,10 +18,12 @@ class Options(object):
1718
*fields : Field
1819
Zero or more configurable fields.
1920
handler : callable, optional
20-
A callable with the signature ``handler(options)``, where ``options``
21-
is the validated dict of user options. Should return a dict of
22-
configuration overrides to forward to the cluster manager. If not
23-
provided, the default will return the options unchanged.
21+
A callable with the signature ``handler(options)`` or
22+
``handler(options, user)``, where ``options`` is the validated dict of
23+
user options, and ``user`` is a ``User`` model for that user. Should
24+
return a dict of configuration overrides to forward to the cluster
25+
manager. If not provided, the default will return the options
26+
unchanged.
2427
2528
Example
2629
-------
@@ -59,6 +62,24 @@ def __init__(self, *fields, handler=None):
5962
self.fields = fields
6063
self.handler = handler
6164

65+
@property
66+
def handler(self):
67+
return self._handler
68+
69+
@handler.setter
70+
def handler(self, handler):
71+
if handler is None:
72+
self._handler = None
73+
else:
74+
sig = inspect.signature(handler)
75+
if len(sig.parameters) == 1 and not any(
76+
a.kind == inspect.Parameter.VAR_POSITIONAL
77+
for a in sig.parameters.values()
78+
):
79+
self._handler = lambda options, user: handler(options)
80+
else:
81+
self._handler = handler
82+
6283
def get_specification(self):
6384
return [f.json_spec() for f in self.fields]
6485

@@ -81,11 +102,11 @@ def transform_options(self, options):
81102
for f in self.fields
82103
}
83104

84-
def get_configuration(self, options):
105+
def get_configuration(self, options, user):
85106
options = self.transform_options(options)
86107
if self.handler is None:
87108
return options
88-
return self.handler(FrozenAttrDict(options))
109+
return self.handler(FrozenAttrDict(options), user)
89110

90111

91112
_field_doc_template = """\

docs/source/cluster-options.rst

+39-8
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ A :class:`dask_gateway_server.options.Options` object takes two arguments:
6969

7070
- ``handler``: An optional handler function for translating the values set by
7171
those options into configuration values to set on the corresponding
72-
:ref:`ClusterConfig <cluster-config>`.
72+
:ref:`ClusterConfig <cluster-config>`. Should have the signature
73+
``handler(options)`` or ``handler(options, user)``, where ``options`` is the
74+
validated dict of user options, and ``user`` is a ``User`` model for that
75+
user.
7376

7477
``Field`` objects provide typed specifications for a user facing option. There
7578
are several different ``Field`` classes available, each representing a
@@ -173,7 +176,7 @@ Different Options per User Group
173176
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
174177

175178
Cluster options may be configured to differ based on the user by providing a
176-
function for :data:`c.Backend.cluster_options`. This function recieves a
179+
function for :data:`c.Backend.cluster_options`. This function receives a
177180
:class:`dask_gateway_server.models.User` object and should return a
178181
:class:`dask_gateway_server.options.Options` object. It may optionally be an
179182
``async`` function.
@@ -196,18 +199,46 @@ member of the "power-users" group.
196199
def generate_options(user):
197200
if "power-users" in user.groups:
198201
options = Options(
199-
Integer("worker_cores", default=1, min=1, max=4, label="Worker Cores"),
200-
Float("worker_memory", default=1, min=1, max=8, label="Worker Memory (GiB)"),
202+
Integer("worker_cores", default=1, min=1, max=8, label="Worker Cores"),
203+
Float("worker_memory", default=1, min=1, max=16, label="Worker Memory (GiB)"),
201204
handler=options_handler,
202-
)
205+
)
203206
else:
204207
options = Options(
205-
Integer("worker_cores", default=1, min=1, max=8, label="Worker Cores"),
206-
Float("worker_memory", default=1, min=1, max=16, label="Worker Memory (GiB)"),
208+
Integer("worker_cores", default=1, min=1, max=4, label="Worker Cores"),
209+
Float("worker_memory", default=1, min=1, max=8, label="Worker Memory (GiB)"),
207210
handler=options_handler,
208-
)
211+
)
209212
210213
c.Backend.cluster_options = generate_options
211214
215+
User-specific Configuration
216+
^^^^^^^^^^^^^^^^^^^^^^^^^^^
217+
218+
Since the ``handler`` function can optionally take in the ``User`` object, you
219+
can use this to add user-specific configuration. Note that you don't have to
220+
expose any configuration options to make use of this, the options handler is
221+
called regardless.
222+
223+
Here we configure the worker cores and memory based on the user's groups:
224+
225+
.. code-block:: python
226+
227+
from dask_gateway_server.options import Options
228+
229+
def options_handler(options, user):
230+
if "power-users" in user.groups:
231+
return {
232+
"worker_cores": 8,
233+
"worker_memory": "16 G"
234+
}
235+
else:
236+
return {
237+
"worker_cores": 4,
238+
"worker_memory": "8 G"
239+
}
240+
241+
c.Backend.cluster_options = Options(handler=options_handler)
242+
212243
213244
.. _ipywidgets: https://ipywidgets.readthedocs.io/en/latest/

tests/test_options.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import dask_gateway.options as client_options
55
import dask_gateway_server.options as server_options
66
from dask_gateway_server.options import FrozenAttrDict
7+
from dask_gateway_server.models import User
78

89

910
def test_string():
@@ -381,19 +382,29 @@ def test_server_options_parse_options(server_opts):
381382
def test_server_options_get_configuration(server_opts):
382383
options = {"integer_field": 2}
383384
sol = {"integer_field2": 2, "float_field": 2.5, "select_field": 5}
385+
user = User("alice")
384386

385387
# Default handler
386-
config = server_opts.get_configuration(options)
388+
config = server_opts.get_configuration(options, user)
387389
assert config == sol
388390

389-
# Custom handler
391+
# Custom handler, no user
390392
def handler(options):
391393
assert isinstance(options, FrozenAttrDict)
392394
assert dict(options) == sol
393395
return {"a": 1}
394396

395397
server_opts.handler = handler
396-
assert server_opts.get_configuration(options) == {"a": 1}
398+
assert server_opts.get_configuration(options, user) == {"a": 1}
399+
400+
# Custom handler, with user
401+
def handler(options, user):
402+
assert isinstance(options, FrozenAttrDict)
403+
assert dict(options) == sol
404+
return {"a": 1, "username": user.name}
405+
406+
server_opts.handler = handler
407+
assert server_opts.get_configuration(options, user) == {"a": 1, "username": "alice"}
397408

398409

399410
@pytest.fixture

0 commit comments

Comments
 (0)