11from __future__ import annotations
2- import random
2+
33import argparse
44import asyncio
5- from enum import Enum
65import json
76import os
7+ import random
88import secrets
9+ import time
910from dataclasses import dataclass
1011from datetime import datetime
1112from functools import partial
12- import time
1313from typing import Any , Coroutine , List
1414
1515import aiohttp
1616import aiometer
1717from playwright .async_api import Browser , async_playwright
1818from yarl import URL
1919
20+
2021@dataclass
2122class TimedResult [T ]:
2223 duration : float
2324 result : T
2425
26+
2527@dataclass
2628class HubAccess :
2729 """
@@ -63,6 +65,7 @@ class FailedServer(Server):
6365 start_failure_time : datetime
6466 startup_events : List [dict ]
6567
68+
6669@dataclass
6770class NBGitpullerURL :
6871 repo : str
@@ -74,7 +77,7 @@ def make_fullpath(self, server_url: URL, targetpath: str) -> URL:
7477 "repo" : self .repo ,
7578 "branch" : self .ref ,
7679 "targetPath" : targetpath ,
77- "urlPath" : os .path .join ("tree" , targetpath , self .open_path )
80+ "urlPath" : os .path .join ("tree" , targetpath , self .open_path ),
7881 }
7982
8083 return (server_url / "git-pull" ).with_query (query_params )
@@ -101,14 +104,19 @@ async def load_nbgitpuller_url(
101104 targetpath = secrets .token_hex (8 )
102105 going_to = nbgitpuller_url .make_fullpath (server .server_url , targetpath )
103106 await page .goto (str (going_to ))
104- expected_final_full_url = str (nbgitpuller_url .make_expectedpath (server .server_url , targetpath ))
107+ expected_final_full_url = str (
108+ nbgitpuller_url .make_expectedpath (server .server_url , targetpath )
109+ )
105110 await page .wait_for_url (expected_final_full_url , timeout = 120 * 10 * 1000 )
106111 await page .wait_for_load_state ("networkidle" )
107112 await page .screenshot (path = screenshot_name )
108113 return TimedResult (time .perf_counter () - start_time , None )
109114
115+
110116async def start_named_server (
111- session : aiohttp .ClientSession , server : Server , profile_options : dict [str , str ] | None = None ,
117+ session : aiohttp .ClientSession ,
118+ server : Server ,
119+ profile_options : dict [str , str ] | None = None ,
112120) -> TimedResult [RunningServer | None ]:
113121 """
114122 Try to start a named server as defined
@@ -124,11 +132,15 @@ async def start_named_server(
124132 / server .servername
125133 )
126134 events = []
127- async with session .post (server_api_url , headers = headers , json = profile_options ) as resp :
135+ async with session .post (
136+ server_api_url , headers = headers , json = profile_options
137+ ) as resp :
128138 if resp .status == 202 :
129139 # we are awaiting start, let's look for events
130140 async with session .get (
131- server_api_url / "progress" , headers = headers , timeout = aiohttp .ClientTimeout (10 * 60 )
141+ server_api_url / "progress" ,
142+ headers = headers ,
143+ timeout = aiohttp .ClientTimeout (10 * 60 ),
132144 ) as progress_resp :
133145 async for line in progress_resp .content :
134146 if line .decode ().strip () == "" :
@@ -145,9 +157,11 @@ async def start_named_server(
145157 hub_access = server .hub_access ,
146158 startup_events = events ,
147159 server_url = URL (
148- server .hub_access .url .joinpath (progress_event ["url" ].lstrip ("/" ), encoded = True )
160+ server .hub_access .url .joinpath (
161+ progress_event ["url" ].lstrip ("/" ), encoded = True
162+ )
149163 ),
150- )
164+ ),
151165 )
152166 elif resp .status == 429 :
153167 # Sleep for upto roughly 2s and try again
@@ -161,14 +175,16 @@ async def start_named_server(
161175 # Some kinda error
162176 resp .raise_for_status ()
163177
178+
164179def poisson_distribution_wait_times (rate : float , count : int ) -> list [float ]:
165180 wait_times = []
166181 t = 0
167- while ( len (wait_times ) < count ) :
182+ while len (wait_times ) < count :
168183 t += random .expovariate (rate )
169184 wait_times .append (t )
170185 return wait_times
171186
187+
172188async def payload (
173189 session : aiohttp .ClientSession ,
174190 browser : Browser ,
@@ -195,23 +211,31 @@ async def payload(
195211 server .servername + ".png" ,
196212 )
197213 timing_info ["nbgitpuller" ] = result .duration
198- print (f"nbgitpuller completed for { server .servername } in { result .duration :0.2f} s" )
214+ print (
215+ f"nbgitpuller completed for { server .servername } in { result .duration :0.2f} s"
216+ )
199217 case None :
200218 print ("Server startup failed" )
201219
202220 print (f"{ server .servername } : { timing_info } " )
203221
222+
204223async def delay (duration : float , callable : Coroutine ) -> Any :
205224 await asyncio .sleep (duration )
206225 return await callable
207226
227+
208228async def main ():
209229 argparser = argparse .ArgumentParser ()
210230 argparser .add_argument ("hub_url" , help = "Full URL to the JupyterHub to test against" )
211- argparser .add_argument ("server_prefix" , help = "Prefix used for named servers started in this run" )
231+ argparser .add_argument (
232+ "server_prefix" , help = "Prefix used for named servers started in this run"
233+ )
212234 argparser .add_argument ("username" , help = "Name of the user" )
213235 argparser .add_argument ("servers_count" , type = int , help = "Number of servers to start" )
214- argparser .add_argument ("server_startup_rate" , type = int , help = "Number of servers to start per minute" )
236+ argparser .add_argument (
237+ "server_startup_rate" , type = int , help = "Number of servers to start per minute"
238+ )
215239 # FIXME: This shouldn't be here.
216240 argparser .add_argument (
217241 "--max-concurrency" ,
@@ -220,36 +244,55 @@ async def main():
220244 help = "Max Numbers of Servers to start at the same time" ,
221245 )
222246 argparser .add_argument (
223- ' --profile-option' ,
247+ " --profile-option" ,
224248 help = "Additional profile option to specify when starting the server (of key=value form)" ,
225- nargs = "*"
249+ nargs = "*" ,
226250 )
227251
228252 args = argparser .parse_args ()
229253
230- nbgitpuller_url = NBGitpullerURL ("https://github.com/mspass-team/mspass_tutorial/" , "master" , "Earthscope2025" )
254+ nbgitpuller_url = NBGitpullerURL (
255+ "https://github.com/mspass-team/mspass_tutorial/" , "master" , "Earthscope2025"
256+ )
231257 token = os .environ ["JUPYTERHUB_TOKEN" ]
232258
233259 profile_options = None
234260 if args .profile_option :
235261 profile_options = {}
236262 for po in args .profile_option :
237- key , value = po .split ('=' , 2 )
263+ key , value = po .split ("=" , 2 )
238264 profile_options [key ] = value
239265
240266 hub_url = URL (args .hub_url )
241267 async with async_playwright () as p :
242268 browser = await p .firefox .launch (headless = False )
243269 async with aiohttp .ClientSession () as session :
244270 servers_to_start = [
245- Server (f"{ args .server_prefix } -{ i } " , args .username , HubAccess (hub_url , token ))
271+ Server (
272+ f"{ args .server_prefix } -{ i } " ,
273+ args .username ,
274+ HubAccess (hub_url , token ),
275+ )
246276 for i in range (args .servers_count )
247277 ]
248- delays = poisson_distribution_wait_times (args .server_startup_rate / 60 , args .servers_count )
278+ delays = poisson_distribution_wait_times (
279+ args .server_startup_rate / 60 , args .servers_count
280+ )
249281 print (delays )
250282 await aiometer .run_all (
251283 [
252- partial (delay , duration , payload (session , browser , token , nbgitpuller_url , profile_options , server ))
284+ partial (
285+ delay ,
286+ duration ,
287+ payload (
288+ session ,
289+ browser ,
290+ token ,
291+ nbgitpuller_url ,
292+ profile_options ,
293+ server ,
294+ ),
295+ )
253296 for server , duration in zip (servers_to_start , delays )
254297 ],
255298 max_at_once = args .max_concurrency ,
0 commit comments