Implement session management - Prevents resource leaks from abandoned sessions
This comprehensive session management system tracks and automatically cleans up resources associated with user sessions, preventing resource exhaustion and disk space issues. Key features: - Automatic tracking of all session resources (audio files, temp files, streams) - Per-session resource limits (100 files max, 100MB storage max) - Automatic cleanup of idle sessions (15 minutes) and expired sessions (1 hour) - Background cleanup thread runs every minute - Real-time monitoring via admin endpoints - CLI commands for manual management - Integration with Flask request lifecycle Implementation details: - SessionManager class manages lifecycle of UserSession objects - Each session tracks resources with metadata (type, size, creation time) - Automatic resource eviction when limits are reached (LRU policy) - Orphaned file detection and cleanup - Thread-safe operations with proper locking - Comprehensive metrics and statistics export - Admin API endpoints for monitoring and control Security considerations: - Sessions tied to IP address and user agent - Admin endpoints require authentication - Secure file path handling - Resource limits prevent DoS attacks This addresses the critical issue of temporary file accumulation that could lead to disk exhaustion in production environments. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
126
app.py
126
app.py
@@ -5,7 +5,7 @@ import requests
|
||||
import json
|
||||
import logging
|
||||
from dotenv import load_dotenv
|
||||
from flask import Flask, render_template, request, jsonify, Response, send_file, send_from_directory, stream_with_context
|
||||
from flask import Flask, render_template, request, jsonify, Response, send_file, send_from_directory, stream_with_context, g
|
||||
from flask_cors import CORS, cross_origin
|
||||
import whisper
|
||||
import torch
|
||||
@@ -35,6 +35,7 @@ logger = logging.getLogger(__name__)
|
||||
# Import configuration and secrets management
|
||||
from config import init_app as init_config
|
||||
from secrets_manager import init_app as init_secrets
|
||||
from session_manager import init_app as init_session_manager, track_resource
|
||||
|
||||
# Error boundary decorator for Flask routes
|
||||
def with_error_boundary(func):
|
||||
@@ -61,6 +62,7 @@ app = Flask(__name__)
|
||||
# Initialize configuration and secrets management
|
||||
init_config(app)
|
||||
init_secrets(app)
|
||||
init_session_manager(app)
|
||||
|
||||
# Configure CORS with security best practices
|
||||
cors_config = {
|
||||
@@ -589,6 +591,7 @@ def index():
|
||||
@app.route('/transcribe', methods=['POST'])
|
||||
@rate_limit(requests_per_minute=10, requests_per_hour=100, check_size=True)
|
||||
@with_error_boundary
|
||||
@track_resource('audio_file')
|
||||
def transcribe():
|
||||
if 'audio' not in request.files:
|
||||
return jsonify({'error': 'No audio file provided'}), 400
|
||||
@@ -610,6 +613,17 @@ def transcribe():
|
||||
temp_path = os.path.join(app.config['UPLOAD_FOLDER'], temp_filename)
|
||||
audio_file.save(temp_path)
|
||||
register_temp_file(temp_path)
|
||||
|
||||
# Add to session resources
|
||||
if hasattr(g, 'session_manager') and hasattr(g, 'user_session'):
|
||||
file_size = os.path.getsize(temp_path)
|
||||
g.session_manager.add_resource(
|
||||
session_id=g.user_session.session_id,
|
||||
resource_type='audio_file',
|
||||
path=temp_path,
|
||||
size_bytes=file_size,
|
||||
metadata={'filename': temp_filename, 'purpose': 'transcription'}
|
||||
)
|
||||
|
||||
try:
|
||||
# Check if we should auto-detect language
|
||||
@@ -857,6 +871,7 @@ def translate_stream():
|
||||
@app.route('/speak', methods=['POST'])
|
||||
@rate_limit(requests_per_minute=15, requests_per_hour=200, check_size=True)
|
||||
@with_error_boundary
|
||||
@track_resource('audio_file')
|
||||
def speak():
|
||||
try:
|
||||
# Validate request size
|
||||
@@ -946,6 +961,17 @@ def speak():
|
||||
|
||||
# Register for cleanup
|
||||
register_temp_file(temp_audio_path)
|
||||
|
||||
# Add to session resources
|
||||
if hasattr(g, 'session_manager') and hasattr(g, 'user_session'):
|
||||
file_size = os.path.getsize(temp_audio_path)
|
||||
g.session_manager.add_resource(
|
||||
session_id=g.user_session.session_id,
|
||||
resource_type='audio_file',
|
||||
path=temp_audio_path,
|
||||
size_bytes=file_size,
|
||||
metadata={'filename': temp_audio_filename, 'purpose': 'tts_output'}
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
@@ -1355,5 +1381,103 @@ def block_ip():
|
||||
logger.error(f"Failed to block IP: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/admin/sessions', methods=['GET'])
|
||||
@rate_limit(requests_per_minute=10)
|
||||
def get_sessions():
|
||||
"""Get information about all active sessions"""
|
||||
try:
|
||||
# Simple authentication check
|
||||
auth_token = request.headers.get('X-Admin-Token')
|
||||
expected_token = app.config.get('ADMIN_TOKEN', 'default-admin-token')
|
||||
|
||||
if auth_token != expected_token:
|
||||
return jsonify({'error': 'Unauthorized'}), 401
|
||||
|
||||
if hasattr(app, 'session_manager'):
|
||||
sessions = app.session_manager.get_all_sessions_info()
|
||||
stats = app.session_manager.get_stats()
|
||||
|
||||
return jsonify({
|
||||
'sessions': sessions,
|
||||
'stats': stats
|
||||
})
|
||||
else:
|
||||
return jsonify({'error': 'Session manager not initialized'}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get sessions: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/admin/sessions/<session_id>', methods=['GET'])
|
||||
@rate_limit(requests_per_minute=20)
|
||||
def get_session_details(session_id):
|
||||
"""Get detailed information about a specific session"""
|
||||
try:
|
||||
# Simple authentication check
|
||||
auth_token = request.headers.get('X-Admin-Token')
|
||||
expected_token = app.config.get('ADMIN_TOKEN', 'default-admin-token')
|
||||
|
||||
if auth_token != expected_token:
|
||||
return jsonify({'error': 'Unauthorized'}), 401
|
||||
|
||||
if hasattr(app, 'session_manager'):
|
||||
session_info = app.session_manager.get_session_info(session_id)
|
||||
if session_info:
|
||||
return jsonify(session_info)
|
||||
else:
|
||||
return jsonify({'error': 'Session not found'}), 404
|
||||
else:
|
||||
return jsonify({'error': 'Session manager not initialized'}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get session details: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/admin/sessions/<session_id>/cleanup', methods=['POST'])
|
||||
@rate_limit(requests_per_minute=5)
|
||||
def cleanup_session(session_id):
|
||||
"""Manually cleanup a specific session"""
|
||||
try:
|
||||
# Simple authentication check
|
||||
auth_token = request.headers.get('X-Admin-Token')
|
||||
expected_token = app.config.get('ADMIN_TOKEN', 'default-admin-token')
|
||||
|
||||
if auth_token != expected_token:
|
||||
return jsonify({'error': 'Unauthorized'}), 401
|
||||
|
||||
if hasattr(app, 'session_manager'):
|
||||
success = app.session_manager.cleanup_session(session_id)
|
||||
if success:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Session {session_id} cleaned up successfully'
|
||||
})
|
||||
else:
|
||||
return jsonify({'error': 'Session not found'}), 404
|
||||
else:
|
||||
return jsonify({'error': 'Session manager not initialized'}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to cleanup session: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/admin/sessions/metrics', methods=['GET'])
|
||||
@rate_limit(requests_per_minute=30)
|
||||
def get_session_metrics():
|
||||
"""Get session management metrics for monitoring"""
|
||||
try:
|
||||
# Simple authentication check
|
||||
auth_token = request.headers.get('X-Admin-Token')
|
||||
expected_token = app.config.get('ADMIN_TOKEN', 'default-admin-token')
|
||||
|
||||
if auth_token != expected_token:
|
||||
return jsonify({'error': 'Unauthorized'}), 401
|
||||
|
||||
if hasattr(app, 'session_manager'):
|
||||
metrics = app.session_manager.export_metrics()
|
||||
return jsonify(metrics)
|
||||
else:
|
||||
return jsonify({'error': 'Session manager not initialized'}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get session metrics: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5005, debug=True)
|
||||
|
||||
Reference in New Issue
Block a user