1+ import argparse
2+ import asyncio
3+ from functools import partial
4+ import aiometer
5+ from datetime import datetime
6+ from typing import List , override
7+ import aiohttp
8+ import os
9+ import json
10+ from yarl import URL
11+ from dataclasses import dataclass
12+
13+
14+ @dataclass
15+ class ServerStartResult :
16+ servername : str
17+ username : str
18+ start_time : datetime
19+ completion_time : datetime
20+ started_successfully : bool
21+ events : List [dict ]
22+
23+ @property
24+ def startup_duration (self ):
25+ return self .completion_time - self .start_time
26+
27+ @override
28+ def __str__ (self ) -> str :
29+ return f"user:{ self .username } server:{ self .servername } started:{ self .started_successfully } startup_duration:{ self .startup_duration } "
30+
31+ async def start_named_server (session : aiohttp .ClientSession , hub_url : URL , username : str , server_name : str ) -> ServerStartResult :
32+ """
33+ Try to start a named server as defined
34+
35+ """
36+ server_api_url = hub_url / "hub/api/users" / username / "servers" / server_name
37+ events = []
38+ async with session .post (server_api_url ) as resp :
39+ start_time = datetime .now ()
40+ if resp .status == 202 :
41+ # we are awaiting start, let's look for events
42+ print (f"server { server_name } waiting to start" )
43+ async with session .get (server_api_url / "progress" ) as progress_resp :
44+ async for line in progress_resp .content :
45+ if line .decode ().strip () == '' :
46+ # Empty line, just continue
47+ continue
48+ progress_event = json .loads (line .decode ().strip ()[len ("data: " ):])
49+ events .append (progress_event )
50+ if progress_event .get ("ready" ) == True :
51+ return ServerStartResult (
52+ username = username ,
53+ servername = server_name ,
54+ start_time = start_time ,
55+ completion_time = datetime .now (),
56+ started_successfully = True ,
57+ events = events
58+ )
59+ elif resp .status == 201 :
60+ # Means the server is immediately ready, and i don't want to deal with that yet
61+ raise NotImplementedError ()
62+
63+
64+ async def main ():
65+ argparser = argparse .ArgumentParser ()
66+ argparser .add_argument ("hub_url" , help = "Full URL to the JupyterHub to test against" )
67+ argparser .add_argument ("username" , help = "Name of the user" )
68+ argparser .add_argument ("servers_count" , type = int , help = "Number of servers to start" )
69+ argparser .add_argument ("--max-concurrency" , type = int , default = 30 , help = "Max Numbers of Servers to start at the same time" )
70+
71+ args = argparser .parse_args ()
72+
73+ token = os .environ ["JUPYTERHUB_TOKEN" ]
74+
75+ hub_url = URL (args .hub_url )
76+ async with aiohttp .ClientSession (headers = {
77+ "Authorization" : f"token { token } "
78+ }) as session :
79+ servernames = [f"perf-test-{ i } " for i in range (args .servers_count )]
80+ async with aiometer .amap (partial (start_named_server , session , hub_url , args .username ), servernames , max_at_once = args .max_concurrency ) as servers :
81+ async for server in servers :
82+ print (server )
83+
84+ if __name__ == "__main__" :
85+ asyncio .run (main ())
0 commit comments