Skip to content

Commit 8ddfcff

Browse files
committed
Replace tornado options with traitlets
1 parent 40ffce4 commit 8ddfcff

File tree

1 file changed

+184
-110
lines changed

1 file changed

+184
-110
lines changed

jupyterhub_idle_culler/__init__.py

Lines changed: 184 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import json
88
import os
99
import ssl
10+
import sys
1011
from datetime import datetime, timezone
1112
from functools import partial
1213
from textwrap import dedent
@@ -18,7 +19,8 @@
1819
from tornado.httputil import url_concat
1920
from tornado.ioloop import IOLoop, PeriodicCallback
2021
from tornado.log import app_log
21-
from tornado.options import define, options, parse_command_line
22+
from traitlets import Bool, Int, Unicode, default
23+
from traitlets.config import Application
2224

2325
__version__ = "1.4.1.dev"
2426

@@ -452,186 +454,258 @@ async def handle_user(user):
452454
app_log.debug("Finished culling %s", name)
453455

454456

455-
def main():
456-
define(
457-
"url",
458-
default=os.environ.get("JUPYTERHUB_API_URL"),
457+
class IdleCuller(Application):
458+
459+
api_page_size = Int(
460+
0,
459461
help=dedent(
460462
"""
461-
The JupyterHub API URL.
463+
Number of users to request per page,
464+
when using JupyterHub 2.0's paginated user list API.
465+
Default: user the server-side default configured page size.
462466
"""
463467
).strip(),
468+
).tag(
469+
config=True,
464470
)
465-
define(
466-
"timeout",
467-
type=int,
468-
default=600,
471+
472+
concurrency = Int(
473+
10,
469474
help=dedent(
470475
"""
471-
The idle timeout (in seconds).
476+
Limit the number of concurrent requests made to the Hub.
477+
478+
Deleting a lot of users at the same time can slow down the Hub,
479+
so limit the number of API requests we have outstanding at any given time.
472480
"""
473481
).strip(),
482+
).tag(
483+
config=True,
474484
)
475-
define(
476-
"cull_every",
477-
type=int,
478-
default=0,
485+
486+
config_file = Unicode(
487+
"idle_culler_config.py",
479488
help=dedent(
480489
"""
481-
The interval (in seconds) for checking for idle servers to cull.
490+
Config file to load.
482491
"""
483492
).strip(),
493+
).tag(
494+
config=True,
484495
)
485-
define(
486-
"max_age",
487-
type=int,
488-
default=0,
496+
497+
cull_admin_users = Bool(
498+
True,
489499
help=dedent(
490500
"""
491-
The maximum age (in seconds) of servers that should be culled even if they are active.
501+
Whether admin users should be culled (only if --cull-users=true).
492502
"""
493503
).strip(),
504+
).tag(
505+
config=True,
494506
)
495-
define(
496-
"cull_users",
497-
type=bool,
498-
default=False,
507+
508+
cull_default_servers = Bool(
509+
True,
499510
help=dedent(
500511
"""
501-
Cull users in addition to servers.
502-
503-
This is for use in temporary-user cases such as tmpnb.
512+
Whether default servers should be culled (only if --cull-default-servers=true).
504513
"""
505514
).strip(),
515+
).tag(
516+
config=True,
506517
)
507-
define(
508-
"remove_named_servers",
509-
default=False,
510-
type=bool,
518+
519+
cull_every = Int(
520+
0,
511521
help=dedent(
512522
"""
513-
Remove named servers in addition to stopping them.
523+
The interval (in seconds) for checking for idle servers to cull.
524+
"""
525+
).strip(),
526+
).tag(
527+
config=True,
528+
)
514529

515-
This is useful for a BinderHub that uses authentication and named servers.
530+
@default("cull_every")
531+
def _default_cull_every(self):
532+
return self.timeout // 2
533+
534+
cull_named_servers = Bool(
535+
True,
536+
help=dedent(
537+
"""
538+
Whether named servers should be culled (only if --cull-named-servers=true).
516539
"""
517540
).strip(),
541+
).tag(
542+
config=True,
518543
)
519-
define(
520-
"concurrency",
521-
type=int,
522-
default=10,
544+
545+
cull_users = Bool(
546+
False,
523547
help=dedent(
524548
"""
525-
Limit the number of concurrent requests made to the Hub.
549+
Cull users in addition to servers.
526550
527-
Deleting a lot of users at the same time can slow down the Hub,
528-
so limit the number of API requests we have outstanding at any given time.
551+
This is for use in temporary-user cases such as tmpnb.
529552
"""
530553
).strip(),
554+
).tag(
555+
config=True,
531556
)
532-
define(
533-
"ssl_enabled",
534-
type=bool,
535-
default=False,
557+
558+
generate_config = Bool(
559+
False,
536560
help=dedent(
537561
"""
538-
Whether the Jupyter API endpoint has TLS enabled.
562+
Generate default config file.
539563
"""
540564
).strip(),
565+
).tag(
566+
config=True,
541567
)
542-
define(
543-
"internal_certs_location",
544-
type=str,
545-
default="internal-ssl",
568+
569+
internal_certs_location = Unicode(
570+
"internal-ssl",
546571
help=dedent(
547572
"""
548573
The location of generated internal-ssl certificates (only needed with --ssl-enabled=true).
549574
"""
550575
).strip(),
576+
).tag(
577+
config=True,
551578
)
552-
define(
553-
"cull_admin_users",
554-
type=bool,
555-
default=True,
579+
580+
max_age = Int(
581+
0,
556582
help=dedent(
557583
"""
558-
Whether admin users should be culled (only if --cull-users=true).
584+
The maximum age (in seconds) of servers that should be culled even if they are active.",
559585
"""
560586
).strip(),
587+
).tag(
588+
config=True,
561589
)
562-
define(
563-
"api_page_size",
564-
type=int,
565-
default=0,
590+
591+
remove_named_servers = Bool(
592+
False,
566593
help=dedent(
567594
"""
568-
Number of users to request per page,
569-
when using JupyterHub 2.0's paginated user list API.
570-
Default: user the server-side default configured page size.
595+
Remove named servers in addition to stopping them.
596+
597+
This is useful for a BinderHub that uses authentication and named servers.
571598
"""
572599
).strip(),
600+
).tag(
601+
config=True,
573602
)
574-
define(
575-
"cull_default_servers",
576-
type=bool,
577-
default=True,
603+
604+
ssl_enabled = Bool(
605+
False,
578606
help=dedent(
579607
"""
580-
Whether default servers should be culled (only if --cull-default-servers=true).
608+
Whether the Jupyter API endpoint has TLS enabled.
581609
"""
582610
).strip(),
611+
).tag(
612+
config=True,
583613
)
584-
define(
585-
"cull_named_servers",
586-
type=bool,
587-
default=True,
614+
615+
timeout = Int(
616+
600,
588617
help=dedent(
589618
"""
590-
Whether named servers should be culled (only if --cull-named-servers=true).
619+
The idle timeout (in seconds).
591620
"""
592621
).strip(),
622+
).tag(
623+
config=True,
593624
)
594625

595-
parse_command_line()
596-
if not options.cull_every:
597-
options.cull_every = options.timeout // 2
598-
api_token = os.environ["JUPYTERHUB_API_TOKEN"]
599-
600-
try:
601-
AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
602-
except ImportError as e:
603-
app_log.warning(
604-
f"Could not load pycurl: {e}\n"
605-
"pycurl is recommended if you have a large number of users."
626+
url = Unicode(
627+
os.environ.get("JUPYTERHUB_API_URL"),
628+
help=dedent(
629+
"""
630+
The JupyterHub API URL.
631+
"""
632+
).strip(),
633+
).tag(
634+
config=True,
635+
)
636+
637+
aliases = {
638+
"api-page-size": "IdleCuller.api_page_size",
639+
"concurrency": "IdleCuller.concurrency",
640+
"cull-admin-users": "IdleCuller.cull_admin_users",
641+
"cull-default-servers": "IdleCuller.cull_default_servers",
642+
"cull-every": "IdleCuller.cull_every",
643+
"cull-named-servers": "IdleCuller.cull_named_servers",
644+
"cull-users": "IdleCuller.cull_users",
645+
"internal-certs-location": "IdleCuller.internal_certs_location",
646+
"max-age": "IdleCuller.max_age",
647+
"remove-named-servers": "IdleCuller.remove_named_servers",
648+
"ssl-enabled": "IdleCuller.ssl_enabled",
649+
"timeout": "IdleCuller.timeout",
650+
"url": "IdleCuller.url",
651+
}
652+
653+
flags = {
654+
"generate-config": (
655+
{"IdleCuller": {"generate_config": True}},
656+
generate_config.help,
606657
)
658+
}
607659

608-
loop = IOLoop.current()
609-
cull = partial(
610-
cull_idle,
611-
url=options.url,
612-
api_token=api_token,
613-
inactive_limit=options.timeout,
614-
cull_users=options.cull_users,
615-
remove_named_servers=options.remove_named_servers,
616-
max_age=options.max_age,
617-
concurrency=options.concurrency,
618-
ssl_enabled=options.ssl_enabled,
619-
internal_certs_location=options.internal_certs_location,
620-
cull_admin_users=options.cull_admin_users,
621-
api_page_size=options.api_page_size,
622-
cull_default_servers=options.cull_default_servers,
623-
cull_named_servers=options.cull_named_servers,
624-
)
625-
# schedule first cull immediately
626-
# because PeriodicCallback doesn't start until the end of the first interval
627-
loop.add_callback(cull)
628-
# schedule periodic cull
629-
pc = PeriodicCallback(cull, 1e3 * options.cull_every)
630-
pc.start()
631-
try:
632-
loop.start()
633-
except KeyboardInterrupt:
634-
pass
660+
def start(self):
661+
662+
if self.generate_config:
663+
print(self.generate_config_file())
664+
sys.exit(0)
665+
666+
if self.config_file:
667+
self.load_config_file(self.config_file)
668+
669+
api_token = os.environ["JUPYTERHUB_API_TOKEN"]
670+
671+
try:
672+
AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
673+
except ImportError as e:
674+
app_log.warning(
675+
f"Could not load pycurl: {e}\n"
676+
"pycurl is recommended if you have a large number of users."
677+
)
678+
679+
loop = IOLoop.current()
680+
cull = partial(
681+
cull_idle,
682+
url=self.url,
683+
api_token=api_token,
684+
inactive_limit=self.timeout,
685+
cull_users=self.cull_users,
686+
remove_named_servers=self.remove_named_servers,
687+
max_age=self.max_age,
688+
concurrency=self.concurrency,
689+
ssl_enabled=self.ssl_enabled,
690+
internal_certs_location=self.internal_certs_location,
691+
cull_admin_users=self.cull_admin_users,
692+
api_page_size=self.api_page_size,
693+
cull_default_servers=self.cull_default_servers,
694+
cull_named_servers=self.cull_named_servers,
695+
)
696+
# schedule first cull immediately
697+
# because PeriodicCallback doesn't start until the end of the first interval
698+
loop.add_callback(cull)
699+
# schedule periodic cull
700+
pc = PeriodicCallback(cull, 1e3 * self.cull_every)
701+
pc.start()
702+
try:
703+
loop.start()
704+
except KeyboardInterrupt:
705+
pass
706+
707+
def main():
708+
IdleCuller.launch_instance()
635709

636710

637711
if __name__ == "__main__":

0 commit comments

Comments
 (0)