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:
2025-06-03 00:47:46 -06:00
parent 9170198c6c
commit eb4f5752ee
5 changed files with 1373 additions and 1 deletions

126
app.py
View File

@@ -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)