Eng-Musa commited on
Commit
7f331eb
·
1 Parent(s): 0e84916

create queue

Browse files
__pycache__/main.cpython-312.pyc CHANGED
Binary files a/__pycache__/main.cpython-312.pyc and b/__pycache__/main.cpython-312.pyc differ
 
main.py CHANGED
@@ -6,7 +6,9 @@ from datetime import datetime, timezone
6
  from pathlib import Path
7
  from typing import Any, Optional
8
 
9
- from fastapi import BackgroundTasks, FastAPI, File, Form, UploadFile
 
 
10
  from fastapi.exceptions import RequestValidationError
11
  from fastapi.responses import JSONResponse
12
  from pydantic import BaseModel, Field
@@ -16,22 +18,30 @@ from services import job_store
16
  from services.cv_chunker import chunk_cv
17
  from services.cv_converter import CVConverter
18
  from services.job_matcher import JobInput, JobMatcher
19
- from services.workers import CvWorker
20
 
21
  # ---------------------------------------------------------------------------
22
  # App setup
23
  # ---------------------------------------------------------------------------
24
 
 
 
 
 
 
 
 
 
 
 
 
25
  app = FastAPI(
26
  title="Job Processor API",
27
  description="CV parsing, chunking, and job-match prediction service.",
28
  version="2.0.0",
 
29
  )
30
 
31
- converter = CVConverter()
32
- matcher = JobMatcher() # SentenceTransformer loaded once at startup
33
- cv_worker = CvWorker(converter)
34
-
35
 
36
  # ---------------------------------------------------------------------------
37
  # Shared response model
@@ -170,7 +180,6 @@ async def process_cv(file: UploadFile = File(...)):
170
 
171
  @app.post("/process-cv-async", response_model=APIResponse)
172
  async def process_cv_async(
173
- background_tasks: BackgroundTasks,
174
  file: UploadFile = File(...),
175
  job_id: str = Form(...),
176
  callback_url: str = Form(...),
@@ -179,7 +188,7 @@ async def process_cv_async(
179
  """
180
  Async entry point called by the Java async worker.
181
  Accepts the CV file + job_id + callback_url as multipart form fields.
182
- Returns 202 immediately; processes the CV in a background task.
183
  """
184
  if not file.filename:
185
  return APIResponse(message="No file uploaded", statusCode=400)
@@ -196,14 +205,14 @@ async def process_cv_async(
196
 
197
  job_store.create_job(job_id)
198
 
199
- background_tasks.add_task(
200
- cv_worker.run_cv_processing,
201
- job_id,
202
- file_bytes,
203
- filename,
204
- callback_url,
205
- callback_secret,
206
  )
 
207
 
208
  return APIResponse(
209
  message="CV processing started",
 
6
  from pathlib import Path
7
  from typing import Any, Optional
8
 
9
+ from contextlib import asynccontextmanager
10
+
11
+ from fastapi import FastAPI, File, Form, UploadFile
12
  from fastapi.exceptions import RequestValidationError
13
  from fastapi.responses import JSONResponse
14
  from pydantic import BaseModel, Field
 
18
  from services.cv_chunker import chunk_cv
19
  from services.cv_converter import CVConverter
20
  from services.job_matcher import JobInput, JobMatcher
21
+ from services.workers import CvWorker, QueueManager, CvTask
22
 
23
  # ---------------------------------------------------------------------------
24
  # App setup
25
  # ---------------------------------------------------------------------------
26
 
27
+ converter = CVConverter()
28
+ matcher = JobMatcher() # SentenceTransformer loaded once at startup
29
+ cv_worker = CvWorker(converter)
30
+ queue_manager = QueueManager(cv_worker, concurrency=2)
31
+
32
+ @asynccontextmanager
33
+ async def lifespan(app: FastAPI):
34
+ await queue_manager.start()
35
+ yield
36
+ await queue_manager.stop()
37
+
38
  app = FastAPI(
39
  title="Job Processor API",
40
  description="CV parsing, chunking, and job-match prediction service.",
41
  version="2.0.0",
42
+ lifespan=lifespan,
43
  )
44
 
 
 
 
 
45
 
46
  # ---------------------------------------------------------------------------
47
  # Shared response model
 
180
 
181
  @app.post("/process-cv-async", response_model=APIResponse)
182
  async def process_cv_async(
 
183
  file: UploadFile = File(...),
184
  job_id: str = Form(...),
185
  callback_url: str = Form(...),
 
188
  """
189
  Async entry point called by the Java async worker.
190
  Accepts the CV file + job_id + callback_url as multipart form fields.
191
+ Returns 202 immediately; processes the CV in a queued background task.
192
  """
193
  if not file.filename:
194
  return APIResponse(message="No file uploaded", statusCode=400)
 
205
 
206
  job_store.create_job(job_id)
207
 
208
+ task = CvTask(
209
+ job_id=job_id,
210
+ file_bytes=file_bytes,
211
+ filename=filename,
212
+ callback_url=callback_url,
213
+ callback_secret=callback_secret
 
214
  )
215
+ await queue_manager.enqueue(task)
216
 
217
  return APIResponse(
218
  message="CV processing started",
services/__pycache__/workers.cpython-312.pyc CHANGED
Binary files a/services/__pycache__/workers.cpython-312.pyc and b/services/__pycache__/workers.cpython-312.pyc differ
 
services/workers.py CHANGED
@@ -6,6 +6,7 @@ import httpx
6
  from datetime import datetime, timezone
7
  from pathlib import Path
8
  from typing import Optional
 
9
 
10
  from services import job_store
11
  from services.cv_chunker import chunk_cv
@@ -111,3 +112,56 @@ class CvWorker:
111
  except Exception as exc:
112
  # Log but don't crash — Java will see job stay in PROCESSING and can time out
113
  print(f"[CV-ASYNC] Failed to POST callback to {callback_url}: {exc}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  from datetime import datetime, timezone
7
  from pathlib import Path
8
  from typing import Optional
9
+ from dataclasses import dataclass
10
 
11
  from services import job_store
12
  from services.cv_chunker import chunk_cv
 
112
  except Exception as exc:
113
  # Log but don't crash — Java will see job stay in PROCESSING and can time out
114
  print(f"[CV-ASYNC] Failed to POST callback to {callback_url}: {exc}")
115
+
116
+ @dataclass
117
+ class CvTask:
118
+ job_id: str
119
+ file_bytes: bytes
120
+ filename: str
121
+ callback_url: str
122
+ callback_secret: str = None
123
+
124
+ class QueueManager:
125
+ """Manages an asyncio Queue with bounded concurrent workers."""
126
+
127
+ def __init__(self, worker: CvWorker, concurrency: int = 2):
128
+ self.worker = worker
129
+ self.concurrency = concurrency
130
+ self.queue = asyncio.Queue()
131
+ self.tasks = []
132
+
133
+ async def start(self):
134
+ """Starts the background worker tasks."""
135
+ for _ in range(self.concurrency):
136
+ task = asyncio.create_task(self._worker_loop())
137
+ self.tasks.append(task)
138
+ print(f"[QUEUE] Started {self.concurrency} concurrent workers.")
139
+
140
+ async def stop(self):
141
+ """Cancels all running workers."""
142
+ for task in self.tasks:
143
+ task.cancel()
144
+ await asyncio.gather(*self.tasks, return_exceptions=True)
145
+ print("[QUEUE] Stopped all workers.")
146
+
147
+ async def _worker_loop(self):
148
+ """Continuously pulls tasks from the queue and processes them."""
149
+ while True:
150
+ try:
151
+ task = await self.queue.get()
152
+ await self.worker.run_cv_processing(
153
+ task.job_id,
154
+ task.file_bytes,
155
+ task.filename,
156
+ task.callback_url,
157
+ task.callback_secret
158
+ )
159
+ self.queue.task_done()
160
+ except asyncio.CancelledError:
161
+ break
162
+ except Exception as e:
163
+ print(f"[QUEUE] Unhandled error in worker loop: {e}")
164
+
165
+ async def enqueue(self, task: CvTask):
166
+ """Adds a new task to the queue."""
167
+ await self.queue.put(task)