diff --git a/README.md b/README.md index 4cfc69d..7a9bf53 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Process live as long as the server live (TODO: a TIMEOUT will be back soon) **Features** * based on [starlette](https://pypi.org/project/starlette/) - * multiple ways to handle sessions (file, mem, etc ...) + * use sessions (multiple ways to handle sessions (file, mem, etc ...)) * compatible with **uvloop** !!! * compatible with multiple gunicorn/uvicorn/webworkers !!! * compatible with [tag.update()](https://manatlan.github.io/htag/tag_update/) @@ -80,7 +80,14 @@ this parameter is available on `app.handle(request, obj, ... http_only=True|Fals #### timeout_interaction (int) It's the time (in seconds) for an interaction (or an initialization) for answering. If the timeout happens : the process/instance is killed. -By default, it's 120 seconds (2 minutes). +By default, it's `60` seconds (1 minute). + +#### timeout_inactivity (int) + +It's the time (in seconds) of inactivities, after that : the process is detroyed. +By default, it's `0` (process lives as long as the server lives). + +IMPORTANT : the "tag.update" feature doesn't reset the inactivity timeout ! #### session_factory (htagweb.sessions) diff --git a/example.py b/example.py index 30553ab..2222e9c 100644 --- a/example.py +++ b/example.py @@ -81,14 +81,11 @@ def test(o): -async def handleJo(req): - return await req.app.handle(req,Jo,http_only=True,parano=True) - #------------------------------------------------------ from htagweb import SimpleServer,AppServer -app=AppServer( App ) -app.add_route("/jo", handleJo ) +app=AppServer( App, timeout_inactivity=0 ) +app.add_route("/jo", lambda req: req.app.handle(req,Jo,http_only=True,parano=True) ) if __name__=="__main__": #~ import logging diff --git a/htagweb/appserver.py b/htagweb/appserver.py index 1398e73..7b93024 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -171,18 +171,20 @@ async def lifespan(app): class AppServer(Starlette): def __init__(self, - obj:"htag.Tag class|fqn|None"=None, + obj:"Tag|fqn|None"=None, session_factory:"sessions.MemDict|sessions.FileDict|sessions.FilePersistentDict|None"=None, debug:bool=True, ssl:bool=False, parano:bool=False, http_only:bool=False, - timeout_interaction=120, + timeout_interaction:int=60, + timeout_inactivity:int=0, ): self.ssl=ssl self.parano = parano self.http_only = http_only self.timeout_interaction = timeout_interaction + self.timeout_inactivity = timeout_inactivity if session_factory is None: self.sesprovider = sessions.MemDict @@ -212,20 +214,22 @@ async def handleHome(request): self.add_route( '/', handleHome ) # DEPRECATED - async def serve(self, request,obj:"htag.Tag|fqn",) -> HTMLResponse: + async def serve(self, request,obj:"Tag|fqn",) -> HTMLResponse: return await self.handle( request, obj) # new method async def handle(self, request, - obj:"htag.Tag|fqn", + obj:"Tag|fqn", http_only:"bool|None"=None, parano:"bool|None"=None, timeout_interaction:"int|None"=None, + timeout_inactivity:"int|None"=None, ) -> HTMLResponse: # take default behaviour if not present is_parano = self.parano if parano is None else parano is_http_only = self.http_only if http_only is None else http_only the_timeout_interaction = self.timeout_interaction if timeout_interaction is None else timeout_interaction + the_timeout_inactivity = self.timeout_inactivity if timeout_inactivity is None else timeout_inactivity uid = request.scope["uid"] args,kargs = commons.url2ak(str(request.url)) @@ -301,7 +305,13 @@ async def handle(self, request, connect(); """ % locals() - hr = HrClient(uid,fqn,js,self.sesprovider.__name__,http_only=is_http_only,timeout_interaction=the_timeout_interaction) + hr = HrClient(uid,fqn, + js = js, + sesprovidername = self.sesprovider.__name__, + http_only = is_http_only, + timeout_interaction = the_timeout_interaction, + timeout_inactivity = the_timeout_inactivity + ) html=await hr.start(*args,**kargs) return HTMLResponse(html) diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index caa0911..414c275 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -12,7 +12,7 @@ import redys import redys.v2 import os,sys,importlib,inspect -import multiprocessing +import time from htag import Tag from htag.render import HRenderer @@ -83,7 +83,7 @@ def __repr__(self): return self.hid ################################################################################## -def hrprocess(hid:Hid,js,init,sesprovidername,useUpdate): +def hrprocess(hid:Hid,js,init,sesprovidername,useUpdate,timeout_inactivity): #TODO: continue here ################################################################################## FactorySession=importFactorySession(sesprovidername) @@ -144,6 +144,7 @@ async def update(actions): #====================================== await registerHrProcess(bus,hid,FactorySession.__name__,pid) + time_activity = time.monotonic() try: # publish the 1st rendering @@ -154,6 +155,7 @@ async def update(actions): while running: params = await bus.get_event( hid.EVENT_INTERACT ) if params is not None: # sometimes it's not a dict ?!? (bool ?!) + time_activity = time.monotonic() if params.get("cmd") == CMD_PS_REUSE: # event REUSE params=params.get("params") @@ -190,6 +192,12 @@ async def update(actions): if not can: log("Can't answer the interact_response for the INTERACT !!!!") + if timeout_inactivity: + if time.monotonic() - time_activity > timeout_inactivity: + log(f"TIMEOUT inactivity ({timeout_inactivity}s), suicide !") + recreate={} + break + await asyncio.sleep(0.1) if recreate: diff --git a/htagweb/server/client.py b/htagweb/server/client.py index 359a134..8e6f31f 100644 --- a/htagweb/server/client.py +++ b/htagweb/server/client.py @@ -22,7 +22,14 @@ def startProcess(params:dict): class HrClient(ServerClient): - def __init__(self,uid:str,fqn:str,js:str=None,sesprovidername=None,http_only=False,timeout_interaction=60): + def __init__(self,uid:str, + fqn:str, + js:str=None, + sesprovidername:str=None, + http_only:bool=False, + timeout_interaction:int=0, + timeout_inactivity:int=0 + ): """ !!!!!!!!!!!!!!!!!!!! if js|sesprovidername is None : can't do a start() !!!!!!!!!!!!!!!!!!!!!!""" ServerClient.__init__(self) @@ -31,7 +38,8 @@ def __init__(self,uid:str,fqn:str,js:str=None,sesprovidername=None,http_only=Fal self.js=js self.sesprovidername=sesprovidername self.useUpdate = not http_only - self.timeout_interaction = timeout_interaction + self.timeout_interaction = timeout_interaction or 60 + self.timeout_inactivity = timeout_inactivity def error(self, *a): txt=f".HrClient {self.hid.uid} {self.hid.fqn}: %s" % (" ".join([str(i) for i in a])) @@ -60,6 +68,7 @@ async def start(self,*a,**k) -> str: init= (a,k), sesprovidername=self.sesprovidername, useUpdate = self.useUpdate, + timeout_inactivity = self.timeout_inactivity ) running_hids:list=await self._bus.get(KEYAPPS) or [] @@ -86,7 +95,7 @@ async def start(self,*a,**k) -> str: # the process has giver a right answer ... return the rendering return message.get("render") - + self.error(f"Event TIMEOUT ({self.timeout_interaction}s) on {self.hid.EVENT_RESPONSE} !!!") await self.kill(self.hid) return f"Timeout: App {self.hid.fqn} killed !" diff --git a/test_server.py b/test_server.py index 2274366..398afdb 100644 --- a/test_server.py +++ b/test_server.py @@ -43,7 +43,7 @@ async def test_base( server ): actions=await p.interact( oid="ut", method_name="doit", args=[], kargs={}, event={} ) assert "update" in actions update_rendering = list(actions["update"].values())[0] - + # session.cpt was incremented at "1" assert ">1" in update_rendering @@ -74,6 +74,30 @@ async def test_app_block_killed( server ): assert fqn not in [i.fqn for i in await p.list()] +@pytest.mark.asyncio +async def test_app_suicide( server ): + # test that a blocking interaction will be stopped + # and app killed + + uid ="u2" + fqn="test_hr:App" + p=HrClient(uid,fqn,"//",timeout_inactivity=1) + html=await p.start() + assert html.startswith("") + + + # app is running + assert fqn in [i.fqn for i in await p.list()] + + + # app will suicide during this period + await asyncio.sleep(2) + + + # app was killed + assert fqn not in [i.fqn for i in await p.list()] + + if __name__=="__main__": s=asyncio.run( startServer())