1111from flask import Flask , request , jsonify , render_template
1212from src .visualiser_graph_generator import generate_graph , generate_output_path
1313from src .visualiser_graph_loader import load_json_file , extract_path_parts , visualiser_graph_file_path
14- from src .utils import update_job_status , read_job_status
14+ from src .utils import (
15+ update_job_status ,
16+ read_job_status ,
17+ get_job_id_for_path ,
18+ get_active_job_status ,
19+ background_run_extraction ,
20+ resume_interrupted_jobs
21+ )
1522from werkzeug .exceptions import BadRequest
1623
1724
@@ -73,8 +80,18 @@ async def extract_quotes():
7380 return jsonify ({"error" : "Missing 'source_path' query parameter" }), 400
7481
7582 input_path , output_path = generate_output_path (source_path )
76- job_id = str ( uuid . uuid4 () )
83+ job_id = get_job_id_for_path ( source_path )
7784
85+ active_status = get_active_job_status (job_id )
86+ if active_status :
87+ logger .info (f"Duplicate request for { source_path } . Job { job_id } is already in progress." )
88+ return jsonify ({
89+ 'job_id' : job_id ,
90+ 'status' : 'already_running' ,
91+ 'message' : f'A graph generation job is already in progress for { source_path } ' ,
92+ 'output_path' : output_path
93+ }), 202
94+
7895 initial_status = {
7996 "job_id" : job_id ,
8097 "status" : "pending" ,
@@ -83,26 +100,7 @@ async def extract_quotes():
83100 }
84101 update_job_status (job_id , initial_status )
85102
86- async def run_extraction ():
87- try :
88- logger .info (f'Starting background graph generation for { input_path } (Job: { job_id } )...' )
89- initial_status ["status" ] = "running"
90- update_job_status (job_id , initial_status )
91-
92- await generate_graph (input_path , output_path )
93-
94- initial_status ["status" ] = "completed"
95- initial_status ["output_path" ] = output_path
96- initial_status ["completed_at" ] = time .time ()
97- update_job_status (job_id , initial_status )
98- logger .info (f'Graph generation completed successfully for { output_path } ' )
99- except Exception as e :
100- logger .error (f"Background graph generation failed for job { job_id } : { str (e )} " )
101- initial_status ["status" ] = "failed"
102- initial_status ["error" ] = str (e )
103- update_job_status (job_id , initial_status )
104-
105- asyncio .create_task (run_extraction ())
103+ asyncio .create_task (background_run_extraction (job_id , input_path , output_path , initial_status ))
106104
107105 return jsonify ({
108106 'job_id' : job_id ,
@@ -129,8 +127,29 @@ def handle_bad_request(e):
129127
130128 return app
131129
130+ class LifespanMiddleware :
131+ """ASGI middleware to handle startup and shutdown events."""
132+ def __init__ (self , app ):
133+ self .app = app
134+
135+ async def __call__ (self , scope , receive , send ):
136+ if scope ["type" ] == "lifespan" :
137+ while True :
138+ message = await receive ()
139+ if message ["type" ] == "lifespan.startup" :
140+ # Trigger resumption when the event loop is officially running
141+ logger .info ("ASGI startup: triggering job resumption scan..." )
142+ asyncio .create_task (resume_interrupted_jobs ())
143+ await send ({"type" : "lifespan.startup.complete" })
144+ elif message ["type" ] == "lifespan.shutdown" :
145+ await send ({"type" : "lifespan.shutdown.complete" })
146+ return
147+ return await self .app (scope , receive , send )
148+
132149def create_asgi_app ():
133- return WsgiToAsgi (create_app ())
150+ flask_app = create_app ()
151+ asgi_app = WsgiToAsgi (flask_app )
152+ return LifespanMiddleware (asgi_app )
134153
135154if __name__ == "__main__" :
136155 asgi_app = create_asgi_app ()
0 commit comments