Skip to content

Commit

Permalink
feat: timeout_inactivity
Browse files Browse the repository at this point in the history
  • Loading branch information
manatlan committed Oct 10, 2023
1 parent 3ec767d commit 163cb88
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 18 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Expand Down Expand Up @@ -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)

Expand Down
7 changes: 2 additions & 5 deletions example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 15 additions & 5 deletions htagweb/appserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)

Expand Down
12 changes: 10 additions & 2 deletions htagweb/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -144,6 +144,7 @@ async def update(actions):
#======================================

await registerHrProcess(bus,hid,FactorySession.__name__,pid)
time_activity = time.monotonic()
try:

# publish the 1st rendering
Expand All @@ -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")
Expand Down Expand Up @@ -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:
Expand Down
15 changes: 12 additions & 3 deletions htagweb/server/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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]))
Expand Down Expand Up @@ -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 []
Expand All @@ -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 !"
Expand Down
26 changes: 25 additions & 1 deletion test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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</cpt2>" in update_rendering

Expand Down Expand Up @@ -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("<!DOCTYPE html><html>")


# 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())
Expand Down

0 comments on commit 163cb88

Please sign in to comment.