Initial commit
This commit is contained in:
514
scripts/app.py
Normal file
514
scripts/app.py
Normal file
@@ -0,0 +1,514 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
FastAPI application with multiple endpoints that publish to NATS JetStream
|
||||
Each endpoint corresponds to an external API that workers will forward to
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
from fastapi import FastAPI, HTTPException, Request, Body
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.requests import Request as StarletteRequest
|
||||
from starlette.responses import Response as StarletteResponse
|
||||
from pydantic import BaseModel
|
||||
import nats
|
||||
from prometheus_client import Counter, Histogram, generate_latest, REGISTRY
|
||||
from starlette.responses import Response
|
||||
import uvicorn
|
||||
from typing import Optional, Dict, Any, List, Union
|
||||
|
||||
# Prometheus metrics
|
||||
request_count = Counter('http_requests_total', 'Total HTTP requests', ['method', 'endpoint', 'status'])
|
||||
request_duration = Histogram('http_request_duration_seconds', 'HTTP request duration', ['method', 'endpoint'])
|
||||
|
||||
app = FastAPI(title="NATS Backend API - Multi-Endpoint", version="1.0.0")
|
||||
|
||||
# Custom CORS middleware to ensure headers are always added
|
||||
class CORSHeaderMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: StarletteRequest, call_next):
|
||||
origin = request.headers.get("origin", "*")
|
||||
response = await call_next(request)
|
||||
response.headers["Access-Control-Allow-Origin"] = origin
|
||||
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
||||
response.headers["Access-Control-Allow-Headers"] = "*"
|
||||
response.headers["Access-Control-Allow-Credentials"] = "false"
|
||||
response.headers["Access-Control-Max-Age"] = "600"
|
||||
return response
|
||||
|
||||
# Add custom CORS middleware first
|
||||
app.add_middleware(CORSHeaderMiddleware)
|
||||
|
||||
# Also add FastAPI's CORS middleware as backup
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origin_regex=r".*", # Match all origins
|
||||
allow_credentials=False,
|
||||
allow_methods=["*"], # Allow all HTTP methods
|
||||
allow_headers=["*"], # Allow all headers
|
||||
expose_headers=["*"], # Expose all headers
|
||||
max_age=600,
|
||||
)
|
||||
|
||||
# NATS connection (will be initialized on startup)
|
||||
nc = None
|
||||
js = None
|
||||
|
||||
# Configuration
|
||||
NATS_URL = os.getenv("NATS_URL", "nats://nats-server:4222")
|
||||
NATS_USER = os.getenv("NATS_USER", "admin")
|
||||
NATS_PASSWORD = os.getenv("NATS_PASSWORD", "package@321#")
|
||||
|
||||
# Endpoint to NATS subject mapping
|
||||
ENDPOINT_ROUTES = {
|
||||
"/live/api/v1/deliveries/createdeliveries": "api.v1.deliveries.createdeliveries",
|
||||
"/live/api/v1/deliveries/updatedelivery": "api.v1.deliveries.updatedelivery",
|
||||
"/live/api/v2/partners/createriderlog": "api.v2.partners.createriderlog",
|
||||
"/live/api/v2/deliveries/createdeliverylog": "api.v2.deliveries.createdeliverylog",
|
||||
"/live/api/v2/partners/createbreaklog": "api.v2.partners.createbreaklog",
|
||||
"/live/api/v2/partners/updatebreaklog": "api.v2.partners.updatebreaklog",
|
||||
"/live/api/v1/mob/orders/createorder": "api.v1.mob.orders.createorder",
|
||||
"/live/api/v1/web/products/create": "api.v1.web.products.create",
|
||||
"/live/api/v1/mob/customers/create": "api.v1.mob.customers.create",
|
||||
"/live/api/v1/mob/customers/login": "api.v1.mob.customers.login",
|
||||
}
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
"""Initialize NATS connection on startup"""
|
||||
global nc, js
|
||||
try:
|
||||
print(f"Connecting to NATS at {NATS_URL}...")
|
||||
nc = await nats.connect(
|
||||
servers=[NATS_URL],
|
||||
user=NATS_USER,
|
||||
password=NATS_PASSWORD,
|
||||
reconnect_time_wait=2,
|
||||
max_reconnect_attempts=10
|
||||
)
|
||||
js = nc.jetstream()
|
||||
print("✅ Connected to NATS JetStream")
|
||||
print(f"✅ Configured {len(ENDPOINT_ROUTES)} endpoint routes")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to connect to NATS: {e}")
|
||||
raise
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown():
|
||||
"""Close NATS connection on shutdown"""
|
||||
global nc
|
||||
if nc:
|
||||
await nc.close()
|
||||
print("NATS connection closed")
|
||||
|
||||
@app.options("/{full_path:path}")
|
||||
async def options_handler(full_path: str, request: Request):
|
||||
"""Handle OPTIONS requests for CORS preflight"""
|
||||
origin = request.headers.get("origin")
|
||||
return JSONResponse(
|
||||
status_code=200,
|
||||
content={},
|
||||
headers={
|
||||
"Access-Control-Allow-Origin": origin if origin else "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
"Access-Control-Allow-Credentials": "false",
|
||||
"Access-Control-Max-Age": "600",
|
||||
}
|
||||
)
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
"""Health check endpoint"""
|
||||
return {"status": "healthy", "nats_connected": nc.is_connected if nc else False}
|
||||
|
||||
@app.get("/ready")
|
||||
async def ready():
|
||||
"""Readiness check endpoint"""
|
||||
if nc and nc.is_connected:
|
||||
return {"status": "ready"}
|
||||
raise HTTPException(status_code=503, detail="Not ready")
|
||||
|
||||
async def publish_to_nats(endpoint: str, data: Union[Dict[str, Any], List[Dict[str, Any]]], request_method: str = "POST"):
|
||||
"""Publish message to NATS with endpoint metadata"""
|
||||
payload = {
|
||||
"endpoint": endpoint,
|
||||
"method": request_method,
|
||||
"data": data,
|
||||
"received_at": int(asyncio.get_event_loop().time() * 1000),
|
||||
"original_path": endpoint
|
||||
}
|
||||
|
||||
# Get NATS subject for this endpoint
|
||||
subject = ENDPOINT_ROUTES.get(endpoint, "api.unknown")
|
||||
|
||||
if js:
|
||||
try:
|
||||
ack = await js.publish(subject, json.dumps(payload).encode())
|
||||
return ack.seq
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to publish to NATS: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to publish message: {str(e)}")
|
||||
else:
|
||||
raise HTTPException(status_code=503, detail="NATS not connected")
|
||||
|
||||
async def publish_to_nats_sync(endpoint: str, data: Union[Dict[str, Any], List[Dict[str, Any]]], request_method: str = "POST", timeout: int = 15):
|
||||
"""
|
||||
Publish message to NATS and wait for a dedicated reply from the worker (Synchronous)
|
||||
"""
|
||||
if not nc or not js:
|
||||
raise HTTPException(status_code=503, detail="NATS not connected")
|
||||
|
||||
# Create a unique inbox for the reply
|
||||
reply_subject = nc.new_inbox()
|
||||
|
||||
# Subscribe to the inbox matching the unique reply subject
|
||||
sub = await nc.subscribe(reply_subject)
|
||||
|
||||
payload = {
|
||||
"endpoint": endpoint,
|
||||
"method": request_method,
|
||||
"data": data,
|
||||
"received_at": int(asyncio.get_event_loop().time() * 1000),
|
||||
"original_path": endpoint,
|
||||
"reply_subject": reply_subject # Tell worker where to reply
|
||||
}
|
||||
|
||||
# Get NATS subject for this endpoint
|
||||
subject = ENDPOINT_ROUTES.get(endpoint, "api.unknown")
|
||||
|
||||
try:
|
||||
# Publish to the Stream
|
||||
ack = await js.publish(subject, json.dumps(payload).encode())
|
||||
print(f"Sync message published to {subject}, waiting for reply on {reply_subject}...")
|
||||
|
||||
# Wait for the worker to process and reply
|
||||
# We assume the worker will send a single JSON message: {"status": <int>, "body": <str|json>}
|
||||
msg = await sub.next_msg(timeout=timeout)
|
||||
|
||||
# Parse the reply
|
||||
response_data = json.loads(msg.data.decode())
|
||||
status_code = response_data.get("status", 500)
|
||||
body_content = response_data.get("body", "")
|
||||
|
||||
# Attempt to parse body as JSON if it's a string, to avoid double-serialization
|
||||
# If the body is already a dict, use it directly.
|
||||
final_content = body_content
|
||||
if isinstance(body_content, str):
|
||||
try:
|
||||
final_content = json.loads(body_content)
|
||||
except:
|
||||
pass # Keep as string if not valid JSON
|
||||
|
||||
await sub.unsubscribe()
|
||||
return status_code, final_content
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
await sub.unsubscribe()
|
||||
print(f"❌ Timeout waiting for sync response on {endpoint}")
|
||||
raise HTTPException(status_code=504, detail="Gateway Timeout: Worker took too long to respond")
|
||||
except Exception as e:
|
||||
await sub.unsubscribe()
|
||||
print(f"❌ Failed during sync processing: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to process synchronous request: {str(e)}")
|
||||
|
||||
# Endpoint 1: Update Delivery (v1) - External API requires PUT
|
||||
@app.put("/live/api/v1/deliveries/updatedelivery")
|
||||
async def update_delivery_v1(data: Dict[str, Any], request: Request):
|
||||
"""Update Delivery endpoint - forwards to NATS (PUT only, as external API requires PUT)"""
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
endpoint = "/live/api/v1/deliveries/updatedelivery"
|
||||
method = "PUT" # Always use PUT for this endpoint
|
||||
|
||||
try:
|
||||
message_id = await publish_to_nats(endpoint, data, method)
|
||||
duration = asyncio.get_event_loop().time() - start_time
|
||||
request_duration.labels(method="PUT", endpoint=endpoint).observe(duration)
|
||||
request_count.labels(method="PUT", endpoint=endpoint, status="200").inc()
|
||||
|
||||
return JSONResponse(
|
||||
status_code=200,
|
||||
content={"status": "accepted", "message_id": message_id, "endpoint": endpoint}
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
request_count.labels(method="PUT", endpoint=endpoint, status="500").inc()
|
||||
print(f"❌ Error processing {endpoint}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# Endpoint: Create Deliveries (v1)
|
||||
@app.post("/live/api/v1/deliveries/createdeliveries")
|
||||
async def create_deliveries_v1(data: Union[Dict[str, Any], List[Dict[str, Any]]], request: Request):
|
||||
"""Create Deliveries endpoint - forwards payload to NATS"""
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
endpoint = "/live/api/v1/deliveries/createdeliveries"
|
||||
method = request.method
|
||||
|
||||
try:
|
||||
message_id = await publish_to_nats(endpoint, data, method)
|
||||
duration = asyncio.get_event_loop().time() - start_time
|
||||
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
|
||||
request_count.labels(method=method, endpoint=endpoint, status="200").inc()
|
||||
|
||||
return JSONResponse(
|
||||
status_code=200,
|
||||
content={"status": "accepted", "message_id": message_id, "endpoint": endpoint}
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
|
||||
print(f"❌ Error processing {endpoint}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# Endpoint 2: Create Rider Log (v2)
|
||||
@app.post("/live/api/v2/partners/createriderlog")
|
||||
async def create_rider_log_v2(data: Dict[str, Any], request: Request):
|
||||
"""Create Rider Log endpoint - forwards to NATS"""
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
endpoint = "/live/api/v2/partners/createriderlog"
|
||||
method = request.method
|
||||
|
||||
try:
|
||||
message_id = await publish_to_nats(endpoint, data, method)
|
||||
duration = asyncio.get_event_loop().time() - start_time
|
||||
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
|
||||
request_count.labels(method=method, endpoint=endpoint, status="200").inc()
|
||||
|
||||
return JSONResponse(
|
||||
status_code=200,
|
||||
content={"status": "accepted", "message_id": message_id, "endpoint": endpoint}
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
|
||||
print(f"❌ Error processing {endpoint}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# Endpoint 3: Create Delivery Log (v2)
|
||||
@app.post("/live/api/v2/deliveries/createdeliverylog")
|
||||
async def create_delivery_log_v2(
|
||||
request: Request,
|
||||
data: List[Dict[str, Any]] = Body(
|
||||
...,
|
||||
example=[{
|
||||
"logid": 0,
|
||||
"tenantid": 1,
|
||||
"partnerid": 44,
|
||||
"locationid": 1,
|
||||
"orderheaderid": 123456,
|
||||
"deliveryid": 654321,
|
||||
"userid": 1111,
|
||||
"orderid": "1-20231624",
|
||||
"orderstatus": "active",
|
||||
"starttime": "2025-12-10 17:51:03",
|
||||
"logdate": "2025-12-10 18:14:04",
|
||||
"latitude": "11.0050664",
|
||||
"longitude": "76.9508776"
|
||||
}]
|
||||
)
|
||||
):
|
||||
"""
|
||||
Create Delivery Log endpoint - forwards to NATS.
|
||||
Accepts either a single dict or a list of dicts to align with external API expectations.
|
||||
"""
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
endpoint = "/live/api/v2/deliveries/createdeliverylog"
|
||||
method = request.method
|
||||
|
||||
try:
|
||||
# Expect only a list of dicts
|
||||
if not isinstance(data, list) or not all(isinstance(item, dict) for item in data):
|
||||
raise HTTPException(status_code=422, detail="Body must be a list of objects")
|
||||
|
||||
normalized_data = data
|
||||
|
||||
message_id = await publish_to_nats(endpoint, normalized_data, method)
|
||||
duration = asyncio.get_event_loop().time() - start_time
|
||||
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
|
||||
request_count.labels(method=method, endpoint=endpoint, status="200").inc()
|
||||
|
||||
return JSONResponse(
|
||||
status_code=200,
|
||||
content={"status": "accepted", "message_id": message_id, "endpoint": endpoint}
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
|
||||
print(f"❌ Error processing {endpoint}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# Endpoint 4: Create Break Rider Log (v2)
|
||||
@app.post("/live/api/v2/partners/createbreaklog")
|
||||
async def create_break_log_v2(data: Dict[str, Any], request: Request):
|
||||
"""Create Break Rider Log endpoint - forwards to NATS"""
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
endpoint = "/live/api/v2/partners/createbreaklog"
|
||||
method = request.method
|
||||
|
||||
try:
|
||||
message_id = await publish_to_nats(endpoint, data, method)
|
||||
duration = asyncio.get_event_loop().time() - start_time
|
||||
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
|
||||
request_count.labels(method=method, endpoint=endpoint, status="200").inc()
|
||||
|
||||
return JSONResponse(
|
||||
status_code=200,
|
||||
content={"status": "accepted", "message_id": message_id, "endpoint": endpoint}
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
|
||||
print(f"❌ Error processing {endpoint}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# Endpoint 5: Update Break Rider Log (v2) - Supports both POST and PUT
|
||||
@app.post("/live/api/v2/partners/updatebreaklog")
|
||||
@app.put("/live/api/v2/partners/updatebreaklog")
|
||||
async def update_break_log_v2(data: Dict[str, Any], request: Request):
|
||||
"""Update Break Rider Log endpoint - forwards to NATS"""
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
endpoint = "/live/api/v2/partners/updatebreaklog"
|
||||
method = request.method # Will be POST or PUT
|
||||
|
||||
try:
|
||||
message_id = await publish_to_nats(endpoint, data, method)
|
||||
duration = asyncio.get_event_loop().time() - start_time
|
||||
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
|
||||
request_count.labels(method=method, endpoint=endpoint, status="200").inc()
|
||||
|
||||
return JSONResponse(
|
||||
status_code=200,
|
||||
content={"status": "accepted", "message_id": message_id, "endpoint": endpoint}
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
|
||||
print(f"❌ Error processing {endpoint}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/metrics")
|
||||
async def metrics():
|
||||
"""Prometheus metrics endpoint"""
|
||||
return Response(content=generate_latest(REGISTRY), media_type="text/plain")
|
||||
|
||||
@app.get("/routes")
|
||||
async def list_routes():
|
||||
"""List all configured routes"""
|
||||
return {
|
||||
"routes": ENDPOINT_ROUTES,
|
||||
"total": len(ENDPOINT_ROUTES)
|
||||
}
|
||||
|
||||
|
||||
# Endpoint 6: Create Order (Mob V1)
|
||||
@app.post("/live/api/v1/mob/orders/createorder")
|
||||
async def create_order_mob_v1(data: Dict[str, Any], request: Request):
|
||||
"""Create Order endpoint (Mobile) - forwards to NATS"""
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
endpoint = "/live/api/v1/mob/orders/createorder"
|
||||
method = request.method
|
||||
|
||||
try:
|
||||
message_id = await publish_to_nats(endpoint, data, method)
|
||||
duration = asyncio.get_event_loop().time() - start_time
|
||||
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
|
||||
request_count.labels(method=method, endpoint=endpoint, status="200").inc()
|
||||
|
||||
return JSONResponse(
|
||||
status_code=200,
|
||||
content={"status": "accepted", "message_id": message_id, "endpoint": endpoint}
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
|
||||
print(f"❌ Error processing {endpoint}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# Endpoint 7: Create Product (Web V1)
|
||||
@app.post("/live/api/v1/web/products/create")
|
||||
async def create_product_web_v1(data: Dict[str, Any], request: Request):
|
||||
"""Create Product endpoint (Web) - forwards to NATS"""
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
endpoint = "/live/api/v1/web/products/create"
|
||||
method = request.method
|
||||
|
||||
try:
|
||||
message_id = await publish_to_nats(endpoint, data, method)
|
||||
duration = asyncio.get_event_loop().time() - start_time
|
||||
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
|
||||
request_count.labels(method=method, endpoint=endpoint, status="200").inc()
|
||||
|
||||
return JSONResponse(
|
||||
status_code=200,
|
||||
content={"status": "accepted", "message_id": message_id, "endpoint": endpoint}
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
|
||||
print(f"❌ Error processing {endpoint}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# Endpoint 8: Create Customer (Mob V1)
|
||||
@app.post("/live/api/v1/mob/customers/create")
|
||||
async def create_customer_v1(data: Dict[str, Any], request: Request):
|
||||
"""Create Customer endpoint (Mobile) - synchronous wait for NATS worker"""
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
endpoint = "/live/api/v1/mob/customers/create"
|
||||
method = request.method
|
||||
|
||||
try:
|
||||
status_code, content = await publish_to_nats_sync(endpoint, data, method)
|
||||
|
||||
duration = asyncio.get_event_loop().time() - start_time
|
||||
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
|
||||
request_count.labels(method=method, endpoint=endpoint, status=str(status_code)).inc()
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status_code,
|
||||
content=content
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
|
||||
print(f"❌ Error processing {endpoint}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# Endpoint 9: Login Customer (Mob V1)
|
||||
@app.post("/live/api/v1/mob/customers/login")
|
||||
async def login_customer_v1(data: Dict[str, Any], request: Request):
|
||||
"""Login Customer endpoint (Mobile) - synchronous wait for NATS worker"""
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
endpoint = "/live/api/v1/mob/customers/login"
|
||||
method = request.method
|
||||
|
||||
try:
|
||||
status_code, content = await publish_to_nats_sync(endpoint, data, method)
|
||||
|
||||
duration = asyncio.get_event_loop().time() - start_time
|
||||
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
|
||||
request_count.labels(method=method, endpoint=endpoint, status=str(status_code)).inc()
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status_code,
|
||||
content=content
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
|
||||
print(f"❌ Error processing {endpoint}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
40
scripts/purge-old-messages.py
Normal file
40
scripts/purge-old-messages.py
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Purge old failed messages from NATS JetStream
|
||||
"""
|
||||
import asyncio
|
||||
import nats
|
||||
import os
|
||||
|
||||
async def purge_messages():
|
||||
nats_url = "nats://nats.workolik.com:4222"
|
||||
nats_user = "admin"
|
||||
nats_password = "package@321#"
|
||||
|
||||
try:
|
||||
print(f"Connecting to NATS at {nats_url}...")
|
||||
nc = await nats.connect(
|
||||
servers=[nats_url],
|
||||
user=nats_user,
|
||||
password=nats_password
|
||||
)
|
||||
print("✅ Connected to NATS")
|
||||
|
||||
js = nc.jetstream()
|
||||
|
||||
# Purge the stream to remove all old messages
|
||||
print("Purging old messages from stream 'EVENTS'...")
|
||||
await js.purge_stream("EVENTS")
|
||||
print("✅ Stream purged - all old messages removed")
|
||||
|
||||
await nc.close()
|
||||
print("✅ Done!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(purge_messages())
|
||||
|
||||
166
scripts/setup_jetstream.py
Normal file
166
scripts/setup_jetstream.py
Normal file
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Setup JetStream stream and consumer for NATS
|
||||
Run this after NATS is deployed and running
|
||||
"""
|
||||
import asyncio
|
||||
import nats
|
||||
import os
|
||||
import sys
|
||||
|
||||
async def setup_jetstream():
|
||||
"""Configure JetStream with two streams (DELIVERIES, RIDER) and per-subject consumers."""
|
||||
nats_url = os.getenv("NATS_URL", "nats://nats.workolik.com:4222")
|
||||
nats_user = os.getenv("NATS_USER", "admin")
|
||||
nats_password = os.getenv("NATS_PASSWORD", "package@321#")
|
||||
|
||||
# Stream definitions
|
||||
streams = {
|
||||
"DELIVERIES": {
|
||||
"subjects": [
|
||||
"api.v1.deliveries.createdeliveries",
|
||||
"api.v1.deliveries.updatedelivery",
|
||||
"api.v2.deliveries.createdeliverylog",
|
||||
],
|
||||
},
|
||||
"RIDER": {
|
||||
"subjects": [
|
||||
"api.v2.partners.createriderlog",
|
||||
"api.v2.partners.createbreaklog",
|
||||
"api.v2.partners.updatebreaklog",
|
||||
],
|
||||
},
|
||||
"ORDERS": {
|
||||
"subjects": [
|
||||
"api.v1.mob.orders.createorder",
|
||||
],
|
||||
},
|
||||
"PRODUCTS": {
|
||||
"subjects": [
|
||||
"api.v1.web.products.create",
|
||||
],
|
||||
},
|
||||
"CUSTOMERS": {
|
||||
"subjects": [
|
||||
"api.v1.mob.customers.login",
|
||||
"api.v1.mob.customers.create",
|
||||
],
|
||||
"retention": "work" # Special handling for Login queue: delete immediately after ack
|
||||
},
|
||||
}
|
||||
|
||||
# Per-subject durable consumers
|
||||
consumers = {
|
||||
"DELIVERIES": {
|
||||
"api.v1.deliveries.createdeliveries": "deliveries_createdeliveries",
|
||||
"api.v1.deliveries.updatedelivery": "deliveries_updatedelivery",
|
||||
"api.v2.deliveries.createdeliverylog": "deliveries_createdeliverylog",
|
||||
},
|
||||
"RIDER": {
|
||||
"api.v2.partners.createriderlog": "rider_createriderlog",
|
||||
"api.v2.partners.createbreaklog": "rider_createbreaklog",
|
||||
"api.v2.partners.updatebreaklog": "rider_updatebreaklog",
|
||||
},
|
||||
"ORDERS": {
|
||||
"api.v1.mob.orders.createorder": "orders_createorder",
|
||||
},
|
||||
"PRODUCTS": {
|
||||
"api.v1.web.products.create": "products_create",
|
||||
},
|
||||
"CUSTOMERS": {
|
||||
"api.v1.mob.customers.login": "customers_login",
|
||||
"api.v1.mob.customers.create": "customers_create",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
print(f"Connecting to NATS at {nats_url}...")
|
||||
nc = await nats.connect(
|
||||
servers=[nats_url],
|
||||
user=nats_user,
|
||||
password=nats_password,
|
||||
)
|
||||
print("✅ Connected to NATS")
|
||||
|
||||
js = nc.jetstream()
|
||||
|
||||
# Create / recreate streams
|
||||
for stream_name, cfg in streams.items():
|
||||
try:
|
||||
info = await js.stream_info(stream_name)
|
||||
print(f"⚠️ Stream '{stream_name}' already exists with subjects={info.config.subjects}, updating...")
|
||||
|
||||
# Determine retention policy
|
||||
retention_policy = cfg.get("retention", "limits")
|
||||
|
||||
await js.update_stream(
|
||||
name=stream_name,
|
||||
subjects=cfg["subjects"],
|
||||
storage="memory",
|
||||
retention=retention_policy,
|
||||
max_age=24 * 60 * 60,
|
||||
max_msgs=50000,
|
||||
max_bytes=512 * 1024 * 1024,
|
||||
)
|
||||
print(f"✅ Stream '{stream_name}' updated")
|
||||
except Exception as e:
|
||||
if "not found" in str(e).lower() or "404" in str(e).lower():
|
||||
print(f"Creating stream '{stream_name}'...")
|
||||
|
||||
# Determine retention policy (use 'limits' by default, 'work' for queues)
|
||||
retention_policy = cfg.get("retention", "limits")
|
||||
|
||||
await js.add_stream(
|
||||
name=stream_name,
|
||||
subjects=cfg["subjects"],
|
||||
storage="memory",
|
||||
retention=retention_policy,
|
||||
max_age=24 * 60 * 60,
|
||||
max_msgs=50000,
|
||||
max_bytes=512 * 1024 * 1024,
|
||||
)
|
||||
print(f"✅ Stream '{stream_name}' created")
|
||||
else:
|
||||
print(f"⚠️ Could not inspect stream '{stream_name}': {e}")
|
||||
|
||||
# Create durable consumers per subject
|
||||
print("\nConfiguring consumers...")
|
||||
for stream_name, subject_map in consumers.items():
|
||||
for subject, durable in subject_map.items():
|
||||
try:
|
||||
print(f"Creating consumer '{durable}' on stream '{stream_name}' for subject '{subject}'...")
|
||||
await js.add_consumer(
|
||||
stream_name,
|
||||
durable_name=durable,
|
||||
filter_subject=subject,
|
||||
ack_policy="explicit",
|
||||
deliver_policy="all",
|
||||
max_deliver=5,
|
||||
ack_wait=30,
|
||||
)
|
||||
print(f"✅ Consumer '{durable}' created")
|
||||
except Exception as e:
|
||||
if "already in use" in str(e).lower() or "already exists" in str(e).lower():
|
||||
print(f"⚠️ Consumer '{durable}' already exists, skipping...")
|
||||
else:
|
||||
raise
|
||||
|
||||
print("\n✅ JetStream setup complete!")
|
||||
print(" Streams:")
|
||||
for name, cfg in streams.items():
|
||||
print(f" - {name}: {', '.join(cfg['subjects'])}")
|
||||
print(" Consumers:")
|
||||
for stream_name, subject_map in consumers.items():
|
||||
for subject, durable in subject_map.items():
|
||||
print(f" - {durable}: stream={stream_name}, subject={subject}")
|
||||
|
||||
await nc.close()
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(setup_jetstream())
|
||||
|
||||
66
scripts/sync_manifests.py
Normal file
66
scripts/sync_manifests.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import os
|
||||
|
||||
def update_yaml_with_script(yaml_path, script_path, key_line_start):
|
||||
with open(script_path, 'r', encoding='utf-8') as f:
|
||||
script_content = f.read()
|
||||
|
||||
# Indent script content by 4 spaces
|
||||
indented_script = '\n'.join(' ' + line if line.strip() else line for line in script_content.splitlines())
|
||||
|
||||
with open(yaml_path, 'r', encoding='utf-8') as f:
|
||||
yaml_lines = f.readlines()
|
||||
|
||||
# Find the key line (e.g., " app.py: |")
|
||||
start_index = -1
|
||||
for i, line in enumerate(yaml_lines):
|
||||
if key_line_start in line:
|
||||
start_index = i + 1
|
||||
break
|
||||
|
||||
if start_index == -1:
|
||||
print(f"Error: Could not find '{key_line_start}' in {yaml_path}")
|
||||
return
|
||||
|
||||
# Find where the script block ends (next line that is NOT indented by at least 4 spaces, or EOF)
|
||||
# Actually, the Data block might be the last thing.
|
||||
# We assume the script goes until the end of the file or next unindented key.
|
||||
# In these files, the script is usually the main data.
|
||||
# Let's just truncate and append if it looks like the script is the last/main thing.
|
||||
# But usually, it's safer to just replace the lines that look like script.
|
||||
|
||||
# Simple heuristic: The script block ends when indentation drops to 2 spaces or 0?
|
||||
# In worker-script.yaml:
|
||||
# 6: data:
|
||||
# 7: worker.py: |
|
||||
# 8: ...script...
|
||||
# The script is indented by 4 spaces.
|
||||
|
||||
pre_script = yaml_lines[:start_index]
|
||||
|
||||
# We will just write the pre_script + indented_script
|
||||
# WARNING: If there are other keys after worker.py, this deletes them.
|
||||
# Let's check the files.
|
||||
# worker-script.yaml: 378 lines. Script ends at 378. Nothing follows.
|
||||
# fiesta-gateway.yaml: 407 lines. Script ends at 407. Nothing follows.
|
||||
# So appending is SAFE.
|
||||
|
||||
with open(yaml_path, 'w', encoding='utf-8') as f:
|
||||
f.writelines(pre_script)
|
||||
f.write(indented_script)
|
||||
f.write('\n') # Ensure newline at EOF
|
||||
|
||||
print(f"Successfully updated {yaml_path}")
|
||||
|
||||
# Update Fiesta Gateway
|
||||
update_yaml_with_script(
|
||||
r'e:\nats\kubernetes\manifests\nearle\fiesta-gateway.yaml',
|
||||
r'e:\nats\kubernetes\conf\app.py',
|
||||
' app.py: |'
|
||||
)
|
||||
|
||||
# Update Worker Script
|
||||
update_yaml_with_script(
|
||||
r'e:\nats\kubernetes\manifests\core\worker-script.yaml',
|
||||
r'e:\nats\kubernetes\conf\worker.py',
|
||||
' worker.py: |'
|
||||
)
|
||||
285
scripts/worker.py
Normal file
285
scripts/worker.py
Normal file
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
NATS Worker that consumes messages and routes them to different external endpoints
|
||||
Based on the endpoint in the message, forwards to the corresponding external API
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import nats
|
||||
from prometheus_client import Counter, Histogram, Gauge, start_http_server
|
||||
import signal
|
||||
import sys
|
||||
|
||||
# Prometheus metrics
|
||||
messages_processed = Counter('worker_messages_processed_total', 'Total messages processed', ['status', 'endpoint'])
|
||||
message_duration = Histogram('worker_message_duration_seconds', 'Message processing duration', ['endpoint'])
|
||||
messages_in_flight = Gauge('worker_messages_in_flight', 'Messages currently being processed')
|
||||
queue_depth = Gauge('worker_queue_depth', 'Approximate queue depth')
|
||||
|
||||
# Configuration
|
||||
NATS_URL = os.getenv("NATS_URL", "nats://nats-server:4222")
|
||||
NATS_USER = os.getenv("NATS_USER", "admin")
|
||||
NATS_PASSWORD = os.getenv("NATS_PASSWORD", "package@321#")
|
||||
NATS_STREAM = os.getenv("NATS_STREAM", "EVENTS")
|
||||
NATS_SUBJECT = os.getenv("NATS_SUBJECT", "api.>") # Subscribe to all API subjects
|
||||
NATS_CONSUMER = os.getenv("NATS_CONSUMER", "worker_consumer")
|
||||
WORKER_CONCURRENCY = int(os.getenv("WORKER_CONCURRENCY", "10"))
|
||||
RETRY_ATTEMPTS = int(os.getenv("RETRY_ATTEMPTS", "5"))
|
||||
RETRY_DELAY = int(os.getenv("RETRY_DELAY_SECONDS", "5"))
|
||||
|
||||
# Base URL for external APIs
|
||||
BASE_URL = os.getenv("EXTERNAL_BASE_URL", "https://jupiter.nearle.app")
|
||||
|
||||
# Endpoint mapping: FastAPI endpoint -> External API endpoint
|
||||
ENDPOINT_MAPPING = {
|
||||
"/live/api/v1/deliveries/createdeliveries": f"{BASE_URL}/live/api/v1/deliveries/createdeliveries",
|
||||
"/live/api/v1/deliveries/updatedelivery": f"{BASE_URL}/live/api/v1/deliveries/updatedelivery",
|
||||
"/live/api/v2/partners/createriderlog": f"{BASE_URL}/live/api/v2/partners/createriderlog",
|
||||
"/live/api/v2/deliveries/createdeliverylog": f"{BASE_URL}/live/api/v2/deliveries/createdeliverylog",
|
||||
"/live/api/v2/partners/createbreaklog": f"{BASE_URL}/live/api/v2/partners/createbreaklog",
|
||||
"/live/api/v2/partners/updatebreaklog": f"{BASE_URL}/live/api/v2/partners/updatebreaklog",
|
||||
}
|
||||
|
||||
# Global state
|
||||
nc = None
|
||||
js = None
|
||||
running = True
|
||||
semaphore = asyncio.Semaphore(WORKER_CONCURRENCY)
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
"""Handle shutdown signals"""
|
||||
global running
|
||||
print("\n🛑 Shutdown signal received, stopping worker...")
|
||||
running = False
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
async def forward_to_external(endpoint: str, payload: dict, api_key: str = None, method: str = "POST") -> bool:
|
||||
"""Forward message to external endpoint with retries"""
|
||||
external_url = ENDPOINT_MAPPING.get(endpoint)
|
||||
|
||||
if not external_url:
|
||||
print(f"⚠️ Unknown endpoint: {endpoint}, skipping...")
|
||||
return False
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
# Get the actual data to forward
|
||||
data_to_forward = payload.get("data", payload)
|
||||
|
||||
# Normalize payload shape for endpoints that expect an array
|
||||
if endpoint == "/live/api/v2/deliveries/createdeliverylog":
|
||||
if isinstance(data_to_forward, dict):
|
||||
data_to_forward = [data_to_forward]
|
||||
elif not isinstance(data_to_forward, list):
|
||||
# Fallback to wrapping anything that's not already list-like
|
||||
data_to_forward = [data_to_forward]
|
||||
|
||||
# Debug logging for delivery log payload shape
|
||||
print(f"➡️ Forwarding createdeliverylog payload: {data_to_forward}")
|
||||
|
||||
# Normalize method to uppercase
|
||||
http_method = method.upper() if method else "POST"
|
||||
|
||||
for attempt in range(RETRY_ATTEMPTS):
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# Use the correct HTTP method
|
||||
if http_method == "GET":
|
||||
async with session.get(
|
||||
external_url,
|
||||
params=data_to_forward if isinstance(data_to_forward, dict) else {},
|
||||
headers=headers,
|
||||
timeout=aiohttp.ClientTimeout(total=30)
|
||||
) as response:
|
||||
return await _handle_response(response, endpoint, attempt)
|
||||
elif http_method == "PUT":
|
||||
async with session.put(
|
||||
external_url,
|
||||
json=data_to_forward,
|
||||
headers=headers,
|
||||
timeout=aiohttp.ClientTimeout(total=30)
|
||||
) as response:
|
||||
return await _handle_response(response, endpoint, attempt)
|
||||
elif http_method == "PATCH":
|
||||
async with session.patch(
|
||||
external_url,
|
||||
json=data_to_forward,
|
||||
headers=headers,
|
||||
timeout=aiohttp.ClientTimeout(total=30)
|
||||
) as response:
|
||||
return await _handle_response(response, endpoint, attempt)
|
||||
elif http_method == "DELETE":
|
||||
async with session.delete(
|
||||
external_url,
|
||||
headers=headers,
|
||||
timeout=aiohttp.ClientTimeout(total=30)
|
||||
) as response:
|
||||
return await _handle_response(response, endpoint, attempt)
|
||||
else: # Default to POST
|
||||
async with session.post(
|
||||
external_url,
|
||||
json=data_to_forward,
|
||||
headers=headers,
|
||||
timeout=aiohttp.ClientTimeout(total=30)
|
||||
) as response:
|
||||
return await _handle_response(response, endpoint, attempt)
|
||||
except asyncio.TimeoutError:
|
||||
print(f"⚠️ Timeout forwarding {endpoint} to external API (attempt {attempt + 1}/{RETRY_ATTEMPTS})")
|
||||
if attempt < RETRY_ATTEMPTS - 1:
|
||||
await asyncio.sleep(RETRY_DELAY * (attempt + 1))
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error forwarding {endpoint} to external API: {e} (attempt {attempt + 1}/{RETRY_ATTEMPTS})")
|
||||
if attempt < RETRY_ATTEMPTS - 1:
|
||||
await asyncio.sleep(RETRY_DELAY * (attempt + 1))
|
||||
|
||||
return False
|
||||
|
||||
async def _handle_response(response, endpoint: str, attempt: int) -> bool:
|
||||
"""Handle HTTP response and determine if we should retry"""
|
||||
body_text = None
|
||||
try:
|
||||
body_text = await response.text()
|
||||
except Exception:
|
||||
body_text = "<no body>"
|
||||
|
||||
if response.status == 200 or response.status == 201:
|
||||
return True
|
||||
elif response.status >= 500:
|
||||
# Server error - retry
|
||||
print(f"⚠️ External API returned {response.status} for {endpoint}, retrying... (attempt {attempt + 1}/{RETRY_ATTEMPTS}) body={body_text}")
|
||||
if attempt < RETRY_ATTEMPTS - 1:
|
||||
await asyncio.sleep(RETRY_DELAY * (attempt + 1)) # Exponential backoff
|
||||
return False
|
||||
else:
|
||||
# Client error - don't retry
|
||||
print(f"❌ External API returned {response.status} for {endpoint}, not retrying; body={body_text}")
|
||||
return False
|
||||
|
||||
async def process_message(msg):
|
||||
"""Process a single message"""
|
||||
async with semaphore:
|
||||
messages_in_flight.inc()
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
endpoint = "unknown"
|
||||
|
||||
try:
|
||||
# Decode message
|
||||
payload = json.loads(msg.data.decode())
|
||||
endpoint = payload.get("endpoint", payload.get("original_path", "unknown"))
|
||||
http_method = payload.get("method", "POST") # Get HTTP method from payload
|
||||
|
||||
print(f"📨 Processing message for endpoint: {endpoint} (method: {http_method})")
|
||||
|
||||
# Forward to external endpoint with correct HTTP method
|
||||
api_key = os.getenv("EXTERNAL_ENDPOINT_API_KEY", "")
|
||||
success = await forward_to_external(endpoint, payload, api_key if api_key else None, http_method)
|
||||
|
||||
duration = asyncio.get_event_loop().time() - start_time
|
||||
message_duration.labels(endpoint=endpoint).observe(duration)
|
||||
|
||||
if success:
|
||||
# Acknowledge message (remove from queue)
|
||||
await msg.ack()
|
||||
messages_processed.labels(status="success", endpoint=endpoint).inc()
|
||||
print(f"✅ Message processed and forwarded successfully for {endpoint}")
|
||||
else:
|
||||
# Negative acknowledge (requeue for retry)
|
||||
await msg.nak()
|
||||
messages_processed.labels(status="failed", endpoint=endpoint).inc()
|
||||
print(f"❌ Failed to forward message for {endpoint}, requeued for retry")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"❌ Invalid JSON in message: {e}")
|
||||
await msg.term() # Terminate (don't retry)
|
||||
messages_processed.labels(status="invalid", endpoint=endpoint).inc()
|
||||
except Exception as e:
|
||||
print(f"❌ Error processing message for {endpoint}: {e}")
|
||||
await msg.nak() # Requeue for retry
|
||||
messages_processed.labels(status="error", endpoint=endpoint).inc()
|
||||
finally:
|
||||
messages_in_flight.dec()
|
||||
|
||||
async def consume_messages():
|
||||
"""Consume messages from NATS JetStream"""
|
||||
global nc, js
|
||||
|
||||
try:
|
||||
print(f"Connecting to NATS at {NATS_URL}...")
|
||||
nc = await nats.connect(
|
||||
servers=[NATS_URL],
|
||||
user=NATS_USER,
|
||||
password=NATS_PASSWORD,
|
||||
reconnect_time_wait=2,
|
||||
max_reconnect_attempts=-1 # Retry forever
|
||||
)
|
||||
js = nc.jetstream()
|
||||
print("✅ Connected to NATS JetStream")
|
||||
|
||||
# Subscribe to messages (using wildcard to catch all API subjects)
|
||||
print(f"Subscribing to stream '{NATS_STREAM}', consumer '{NATS_CONSUMER}'...")
|
||||
print(f" Subject pattern: {NATS_SUBJECT}")
|
||||
print(f" Endpoints configured: {len(ENDPOINT_MAPPING)}")
|
||||
|
||||
psub = await js.pull_subscribe(
|
||||
NATS_SUBJECT,
|
||||
NATS_CONSUMER,
|
||||
stream=NATS_STREAM
|
||||
)
|
||||
print(f"✅ Subscribed, waiting for messages...")
|
||||
|
||||
# Start metrics server
|
||||
start_http_server(9090)
|
||||
print("✅ Metrics server started on port 9090")
|
||||
|
||||
# Process messages in a loop
|
||||
while running:
|
||||
try:
|
||||
# Fetch messages (batch of up to 10)
|
||||
msgs = await psub.fetch(10, timeout=5)
|
||||
for msg in msgs:
|
||||
# Process asynchronously
|
||||
asyncio.create_task(process_message(msg))
|
||||
except asyncio.TimeoutError:
|
||||
# No messages available, continue
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error fetching messages: {e}")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Fatal error: {e}")
|
||||
raise
|
||||
finally:
|
||||
if nc:
|
||||
await nc.close()
|
||||
print("NATS connection closed")
|
||||
|
||||
async def main():
|
||||
"""Main entry point"""
|
||||
print("🚀 Starting NATS Worker (Multi-Endpoint)...")
|
||||
print(f" External Base URL: {BASE_URL}")
|
||||
print(f" Concurrency: {WORKER_CONCURRENCY}")
|
||||
print(f" Retry Attempts: {RETRY_ATTEMPTS}")
|
||||
print(f" Configured Endpoints:")
|
||||
for fastapi_endpoint, external_url in ENDPOINT_MAPPING.items():
|
||||
print(f" {fastapi_endpoint} → {external_url}")
|
||||
|
||||
try:
|
||||
await consume_messages()
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 Worker stopped by user")
|
||||
except Exception as e:
|
||||
print(f"❌ Worker crashed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
Reference in New Issue
Block a user