2020from tornado .httputil import url_concat
2121from tornado .ioloop import IOLoop , PeriodicCallback
2222from tornado .log import LogFormatter
23- from traitlets import Bool , Int , Unicode , default
23+ from traitlets import Bool , Callable , Int , Unicode , default
2424from traitlets .config import Application
2525
26+ from .utils import maybe_future
27+
2628__version__ = "1.4.1.dev"
2729
2830STATE_FILTER_MIN_VERSION = V ("1.3.0" )
@@ -78,6 +80,11 @@ def utcnow():
7880 return datetime .now (timezone .utc )
7981
8082
83+ def default_cull_arbiter (* , inactive , inactive_limit , server , ** kwargs ):
84+ """Return True if time inactive exceeds limit, the classic cull check."""
85+ return inactive .total_seconds () >= inactive_limit
86+
87+
8188async def cull_idle (
8289 url ,
8390 api_token ,
@@ -93,6 +100,7 @@ async def cull_idle(
93100 api_page_size = 0 ,
94101 cull_default_servers = True ,
95102 cull_named_servers = True ,
103+ cull_arbiter = default_cull_arbiter ,
96104):
97105 """Shutdown idle single-user servers
98106
@@ -226,26 +234,18 @@ async def handle_server(user, server_name, server, max_age, inactive_limit):
226234 # for running servers
227235 inactive = age
228236
229- # CUSTOM CULLING TEST CODE HERE
230- # Add in additional server tests here. Return False to mean "don't
231- # cull", True means "cull immediately", or, for example, update some
232- # other variables like inactive_limit.
233- #
234- # Here, server['state'] is the result of the get_state method
235- # on the spawner. This does *not* contain the below by
236- # default, you may have to modify your spawner to make this
237- # work. The `user` variable is the user model from the API.
238- #
239- # if server['state']['profile_name'] == 'unlimited'
240- # return False
241- # inactive_limit = server['state']['culltime']
242-
243237 is_default_server = server_name == ""
244238 is_named_server = server_name != ""
245239
240+ cull_result = await maybe_future (
241+ cull_arbiter (
242+ inactive = inactive , inactive_limit = inactive_limit , server = server
243+ )
244+ )
245+
246246 should_cull = (
247247 inactive is not None
248- and inactive . total_seconds () >= inactive_limit
248+ and cull_result
249249 and (
250250 (cull_default_servers and is_default_server )
251251 or (cull_named_servers and is_named_server )
@@ -508,6 +508,36 @@ class IdleCuller(Application):
508508 config = True ,
509509 )
510510
511+ cull_arbiter_hook = Callable (
512+ default_cull_arbiter ,
513+ help = dedent ("""
514+ Enable custom culling logic.
515+
516+ By default, the idle culler compares a server's time since last
517+ activity with a specified idle time limit. This hook allows for
518+ additional or more arbitrary logic when deciding whether to cull a
519+ server or not. To customize culling logic, define a callable taking
520+ 3 arguments:
521+
522+ def my_cull_arbiter(inactive, inactive_limit, server):
523+ if server['state']['profile_name'] == 'unlimited':
524+ return False
525+ return inactive.total_seconds() >= inactive_limit
526+ c.IdleCuller.cull_arbiter_hook = my_cull_arbiter
527+
528+ - 'inactive' is the server's time since last activity, a timedelta object
529+ - 'inactive_limit' is the idle timeout limit, in seconds
530+ - 'server' is the server being considered for culling
531+
532+ This callable should return True if the server should be culled, and
533+ False if it should not. In this example, servers with a profile
534+ name of "unlimited" are never culled, but all others are subject to
535+ the default time limit logic.
536+ """ ).strip (),
537+ ).tag (
538+ config = True ,
539+ )
540+
511541 cull_every = Int (
512542 0 ,
513543 help = dedent ("""
@@ -658,6 +688,8 @@ def start(self):
658688
659689 api_token = os .environ ["JUPYTERHUB_API_TOKEN" ]
660690
691+ cull_arbiter = self .cull_arbiter_hook
692+
661693 try :
662694 AsyncHTTPClient .configure ("tornado.curl_httpclient.CurlAsyncHTTPClient" )
663695 except ImportError as e :
@@ -683,6 +715,7 @@ def start(self):
683715 api_page_size = self .api_page_size ,
684716 cull_default_servers = self .cull_default_servers ,
685717 cull_named_servers = self .cull_named_servers ,
718+ cull_arbiter = cull_arbiter ,
686719 )
687720 # schedule first cull immediately
688721 # because PeriodicCallback doesn't start until the end of the first interval
0 commit comments