| from flask import Flask, request, Response, stream_with_context |
| import requests |
| import random |
| import os |
| import json |
| import time |
| import threading |
|
|
| app = Flask(__name__) |
|
|
| ROUTE_PREFIX = "/hf" |
|
|
| def get_backend_nodes(): |
| nodes_str = os.getenv("BACKEND_NODES", '[]') |
| try: |
| return json.loads(nodes_str) |
| except json.JSONDecodeError: |
| return [] |
|
|
| NODES = get_backend_nodes() |
|
|
| |
| DISABLED_NODES = {} |
| disabled_nodes_lock = threading.Lock() |
|
|
| def get_active_nodes(): |
| now = time.time() |
| with disabled_nodes_lock: |
| active_nodes = [ |
| node for node in NODES |
| if node not in DISABLED_NODES or DISABLED_NODES[node] <= now |
| ] |
| expired_nodes = [node for node, expire_time in DISABLED_NODES.items() if expire_time <= now] |
| for node in expired_nodes: |
| del DISABLED_NODES[node] |
| |
| return active_nodes |
| |
|
|
| @app.route(f'{ROUTE_PREFIX}/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) |
| def load_balancer(path): |
| active_nodes = get_active_nodes() |
| |
| if not active_nodes: |
| return "Error: No active backend nodes available at the moment.", 503 |
|
|
| available_nodes = list(active_nodes) |
| req_json = request.get_json(silent=True) if request.method in ['POST', 'PUT', 'PATCH'] else None |
|
|
| ESSENTIAL_HEADERS = {'content-type', 'accept', 'user-agent'} |
| headers = {k: v for k, v in request.headers.items() if k.lower() in ESSENTIAL_HEADERS} |
|
|
| while available_nodes: |
| chosen_node = random.choice(available_nodes) |
| target_url = f"{chosen_node.rstrip('/')}/{path}" |
| |
| try: |
| print(f"[Info] Routing request to: {chosen_node}") |
| response = requests.request( |
| method=request.method, |
| url=target_url, |
| headers=headers, |
| params=request.args, |
| json=req_json, |
| stream=True, |
| timeout=120 |
| ) |
| |
| |
| if response.status_code == 200: |
| |
| def generate(): |
| try: |
| for chunk in response.iter_content(chunk_size=8192): |
| yield chunk |
| except requests.exceptions.ChunkedEncodingError as e: |
| print(f"[Warning] Chunked stream ended prematurely from {chosen_node}: {e}") |
| return |
| except Exception as e: |
| print(f"[Error] Unexpected error during streaming: {e}") |
| return |
|
|
| proxy_response = Response( |
| stream_with_context(generate()), |
| status=response.status_code |
| ) |
|
|
| for key, value in response.headers.items(): |
| if key.lower() not in ('content-length', 'transfer-encoding', 'connection'): |
| proxy_response.headers[key] = value |
| |
| return proxy_response |
| |
| elif response.status_code == 429: |
| |
| print(f"[Warning] Node {chosen_node} returned 429. Keeping it in pool and retrying...") |
| time.sleep(0.5) |
| continue |
| |
| else: |
| |
| if response.status_code == 403: |
| |
| cooldown_seconds = 1 * 60 |
| expire_time = time.time() + cooldown_seconds |
| with disabled_nodes_lock: |
| DISABLED_NODES[chosen_node] = expire_time |
| print(f"[Warning] Node {chosen_node} failed with 403. Disabling it globally for 1m.") |
| else: |
| print(f"[Warning] Node {chosen_node} returned status {response.status_code}. Retrying another node...") |
| |
| available_nodes.remove(chosen_node) |
| continue |
| |
| except requests.RequestException as e: |
| |
| print(f"[Error] Network issue/Timeout with {chosen_node}: {e}. Retrying another node...") |
| available_nodes.remove(chosen_node) |
|
|
| return "Error: All available backend nodes failed (due to non-200 status or timeout).", 502 |
|
|
|
|
| @app.route('/', methods=['GET']) |
| def index(): |
| return f"Load Balancer Running." |
|
|
| if __name__ == '__main__': |
| app.run(host='0.0.0.0', port=7860, debug=False) |