[Python] Flask ML App Stuck on "Loading" Status Despite Successful Model Training

Discussão em 'Python' iniciado por Stack, Agosto 30, 2025.

  1. Stack

    Stack Membro Participativo

    Problem Statement


    I'm deploying a Flask ML application with book recommendations to Render, but I'm experiencing a persistent issue where my health endpoint always returns "model_loaded": false, "status": "loading" even though the logs clearly show that model training completed successfully.

    Environment Details


    Platform: Render (free tier)

    Framework: Flask + Gunicorn

    ML Stack: Scikit-learn, Pandas, NumPy

    Dataset: 50,000 books from CSV file

    Python Version: 3.13

    Deployment: GitHub → Render auto-deploy

    requirement.txt


    Flask==3.1.1
    flask-cors==6.0.1
    scikit-learn==1.7.0
    pandas==2.3.1
    numpy==2.3.1
    requests==2.32.4
    PyJWT==2.10.1
    Werkzeug==3.1.3
    python-dotenv==1.1.1
    gunicorn==20.1.0
    scipy==1.16.0
    setuptools>=70.0.0
    wheel>=0.40.0

    Expected vs Actual Behavior


    Expected Behavior:
    After model loading completes, the /api/health endpoint should return:

    json
    {
    "status": "healthy",
    "model_loaded": true,
    "data_loading": false,
    "data_size": 50000
    }

    Actual Behavior:


    Despite successful model training logs, the endpoint continues returning:

    json
    {
    "data_loading": true,
    "data_size": 0,
    "features": {
    "analytics": true,
    "collaborative_filtering": false,
    "content_based": false,
    "google_books_api": false,
    "user_authentication": true
    },
    "model_loaded": false,
    "status": "loading",
    "timestamp": "2025-08-30T06:01:39.408897"
    }

    Deployment Logs Show Successful Completion


    The Render logs clearly indicate successful model training:
    ​

    text
    STARTUP LOADING: Starting model and data loading...
    Creating preprocessed data (first time)...
    PREPROCESSING: Creating optimized dataset...
    Reading CSV in chunks...
    Loaded dataset: 100000 books
    Selected 50000 representative books
    ✅ PREPROCESSING: Saved optimized dataset in 43.38s
    Training models (first time)...
    Creating TF-IDF vectorizer...
    Created feature matrix: (50000, 5000)
    Training content-based recommendation model...
    ✅ Content-based model trained!
    Creating collaborative filtering model...
    ⚠️ Warning: Could not create collaborative filtering model: probabilities do not sum to 1
    Saving models for future startups...
    ✅ Models saved successfully!
    STARTUP LOADING: Complete in 58.49s!
    Ready with 50000 books and trained models!

    Current Implemation

    Load environment variables


    load_dotenv()

    app = Flask(__name__)

    # Use environment variables for secrets
    app.secret_key = os.getenv('SECRET_KEY', 'fallback-secret-key-for-development')

    # Update CORS to include your deployed frontend URL
    CORS(app,
    origins=[
    "http://localhost:3000",
    "http://127.0.0.1:3000",
    "https://*.vercel.app", # Allow all Vercel deployments

    ],
    methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
    allow_headers=['Content-Type', 'Authorization'],
    supports_credentials=True,
    max_age=86400
    )

    basedir = os.path.abspath(os.path.dirname(__file__))
    CSV_PATH = os.path.join(basedir, 'Books.csv')
    DB_PATH = os.path.join(basedir, 'readly.db')

    # Optimized file paths for preprocessed data
    PROCESSED_DATA_PATH = os.path.join(basedir, 'processed_data.pkl')
    FEATURES_PATH = os.path.join(basedir, 'features.pkl')
    MODELS_PATH = os.path.join(basedir, 'models.pkl')

    # Global variables for models - STARTUP LOADING
    content_model = None
    collaborative_model = None
    data = None
    user_item_matrix = None
    book_features = None
    vectorizer = None
    models_loaded = False
    models_loading = False
    loading_lock = threading.Lock()

    Model Loading (Background Thread):


    def load_models_at_startup():
    """Load all models and data at startup - NO LAZY LOADING"""
    global content_model, collaborative_model, data, user_item_matrix, book_features, vectorizer
    global models_loaded, models_loading

    with loading_lock:
    if models_loaded or models_loading:
    return models_loaded

    models_loading = True

    try:
    print(" STARTUP LOADING: Starting model and data loading...")
    start_time = time.time()

    # Try to load preprocessed data first
    if os.path.exists(PROCESSED_DATA_PATH):
    print(" Loading preprocessed data...")
    with open(PROCESSED_DATA_PATH, 'rb') as f:
    data = pickle.load(f)
    print(f"✅ Loaded {len(data)} preprocessed books")
    else:
    print(" Creating preprocessed data (first time)...")
    data = create_optimized_dataset()
    if data is None:
    raise Exception("Failed to create dataset")

    # Check if models are already saved
    if os.path.exists(MODELS_PATH) and os.path.exists(FEATURES_PATH):
    print(" Loading pre-trained models...")

    with open(MODELS_PATH, 'rb') as f:
    model_data = pickle.load(f)
    content_model = model_data['content_model']
    vectorizer = model_data['vectorizer']
    collaborative_model = model_data.get('collaborative_model')
    user_item_matrix = model_data.get('user_item_matrix')

    with open(FEATURES_PATH, 'rb') as f:
    book_features = pickle.load(f)

    print("✅ Pre-trained models loaded successfully!")
    else:
    print(" Training models (first time)...")

    # Create TF-IDF vectorizer (optimized)
    print(" Creating TF-IDF vectorizer...")
    vectorizer = TfidfVectorizer(
    stop_words='english',
    max_features=5000, # Reduced from 8000
    ngram_range=(1, 2), # Reduced from (1, 3)
    min_df=3,
    max_df=0.9,
    lowercase=True,
    strip_accents='unicode'
    )

    book_features = vectorizer.fit_transform(data['combined_features'])
    print(f" Created feature matrix: {book_features.shape}")

    # Content-based model with optimized parameters
    print(" Training content-based recommendation model...")
    content_model = NearestNeighbors(
    metric='cosine',
    algorithm='brute',
    n_neighbors=min(20, len(data)) # Reduced neighbors
    )
    content_model.fit(book_features)
    print("✅ Content-based model trained!")

    # Create collaborative filtering model
    print(" Creating collaborative filtering model...")
    try:
    n_users = min(500, len(data)) # Reduced users
    n_books = len(data)

    # Generate synthetic user-book interaction matrix
    np.random.seed(42)
    user_book_ratings = np.random.choice([0, 1, 2, 3, 4, 5],
    size=(n_users, n_books),
    p=[0.8, 0.04, 0.04, 0.08, 0.04, 0.04])

    user_item_matrix = csr_matrix(user_book_ratings)

    # Use SVD for dimensionality reduction
    if user_item_matrix.shape[0] > 20 and user_item_matrix.shape[1] > 20:
    collaborative_model = TruncatedSVD(n_components=30, random_state=42)
    collaborative_model.fit(user_item_matrix)
    print("✅ Collaborative filtering model created successfully!")
    else:
    print("⚠️ Dataset too small for collaborative filtering")
    collaborative_model = None

    except Exception as e:
    print(f"⚠️ Warning: Could not create collaborative filtering model: {e}")
    collaborative_model = None
    user_item_matrix = None

    # Save models for next startup
    print(" Saving models for future startups...")
    model_data = {
    'content_model': content_model,
    'vectorizer': vectorizer,
    'collaborative_model': collaborative_model,
    'user_item_matrix': user_item_matrix
    }

    with open(MODELS_PATH, 'wb') as f:
    pickle.dump(model_data, f)

    with open(FEATURES_PATH, 'wb') as f:
    pickle.dump(book_features, f)

    print("✅ Models saved successfully!")

    models_loaded = True
    models_loading = False

    total_time = time.time() - start_time
    print(f" STARTUP LOADING: Complete in {total_time:.2f}s!")
    print(f" Ready with {len(data)} books and trained models!")

    return True

    except Exception as e:
    print(f"❌ STARTUP LOADING ERROR: {e}")
    import traceback
    traceback.print_exc()
    models_loading = False
    return False

    Health Check Endpoint:


    @app.route('/api/health')
    @cross_origin()
    def health():
    try:
    return jsonify({
    'status': 'healthy' if models_loaded else ('loading' if models_loading else 'ready'),
    'model_loaded': models_loaded,
    'data_loading': models_loading,
    'data_size': len(data) if data is not None else 0
    })
    except Exception as e:
    return jsonify({'status': 'error', 'error': str(e)}), 500

    other endpoints


    @app.route('/api/recommend')
    @cross_origin()
    @models_ready_required
    def api_recommend():
    """Enhanced book recommendations with multiple methods - NO LAZY LOADING"""
    query = request.args.get('q', '')
    method = request.args.get('method', 'content') # content, collaborative, hybrid
    limit = min(int(request.args.get('limit', 8)), 20) # Max 20 recommendations

    print(f" Recommendation request: '{query}' (method: {method}, limit: {limit})")

    if not query:
    return jsonify({
    'error': 'No query provided',
    'query': '',
    'recommendations': [],
    'count': 0
    }), 400

    try:
    # Get user ID if authenticated
    user_id = None
    token = request.headers.get('Authorization')
    if token and token.startswith('Bearer '):
    user_info = verify_auth_token(token[7:])
    if user_info:
    user_id = user_info['user_id']

    recommendations = recommend(query, method, user_id, limit)

    print(f"✅ Found {len(recommendations)} recommendations")

    # Log search analytics
    try:
    with get_db_connection() as conn:
    conn.execute(
    'INSERT INTO searches (query, timestamp, results_count) VALUES (?, ?, ?)',
    (query, get_current_timestamp(), len(recommendations))
    )
    conn.commit()
    except Exception as e:
    print(f"⚠️ Error logging search: {e}")

    return jsonify({
    'query': query,
    'method': method,
    'recommendations': recommendations,
    'count': len(recommendations)
    })

    except Exception as e:
    print(f"❌ Error in api_recommend: {e}")
    return jsonify({
    'error': str(e),
    'query': query,
    'recommendations': [],
    'count': 0
    }), 500

    @app.route('/api/book/<path:book_title>')
    @cross_origin()
    @models_ready_required
    def get_book_details(book_title):
    """Get detailed book information from multiple sources"""
    print(f" Getting details for: {book_title}")

    try:
    book_details = fetch_book_details_combined(book_title)

    # Log book view analytics
    try:
    with get_db_connection() as conn:
    conn.execute(
    '''INSERT OR REPLACE INTO book_analytics
    (book_title, view_count, last_accessed)
    VALUES (?, COALESCE((SELECT view_count FROM book_analytics WHERE book_title = ?) + 1, 1), ?)''',
    (book_title, book_title, get_current_timestamp())
    )
    conn.commit()
    except Exception as e:
    print(f"⚠️ Error logging book analytics: {e}")

    return jsonify(book_details)

    except Exception as e:
    print(f"❌ Error getting book details: {e}")
    return jsonify({
    'error': f'Failed to fetch book details: {str(e)}',
    'title': book_title
    }), 500

    Gunicorn Configuration:


    gunicorn --bind 0.0.0.0:$PORT app:app --timeout 600 --workers 1 --threads 4

    Troubleshooting Steps Attempted


    1.Increased Timeouts Changed Gunicorn timeout from 180s to 600s
    Added longer timeout periods for model loading

    2. Threading Debugging Added thread locks (threading.Lock())
    Implemented thread-safe global variable updates
    Added detailed logging throughout the loading process

    3. Resource Optimization Reduced dataset from 271K to 50K books
    Optimized TF-IDF parameters (max_features=5000)
    Implemented data chunking for memory efficiency

    Error Patterns Observed


    1. Logs show successful completion but health endpoint returns loading status


    2. No exceptions or error messages in the loading process


    3. Global variables remain in initial state despite thread completion


    4. 502 Bad Gateway errors occur intermittently during deployment
    Questions


    1. How can I ensure proper synchronization between the background loading thread and Flask's main thread?


    2. Is there a better pattern for loading ML models in Flask + Gunicorn deployments?


    3. Why it is working ?? am i doing something wrong

    Continue reading...

Compartilhe esta Página