Skip to content

Commit 9f0c388

Browse files
Merge pull request #26 from GitTimeraider/develop
Develop to main
2 parents 50d5d97 + e0a313c commit 9f0c388

File tree

5 files changed

+85
-17
lines changed

5 files changed

+85
-17
lines changed

app/currency.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def get_exchange_rates(self, base_currency: str = 'EUR', force_refresh: bool = F
122122

123123
def _fetch_frankfurter(self):
124124
url = 'https://api.frankfurter.app/latest?from=EUR'
125-
r = requests.get(url, timeout=5) # Reduced timeout from 10 to 5 seconds
125+
r = requests.get(url, timeout=3) # Further reduced timeout from 5 to 3 seconds
126126
r.raise_for_status()
127127
data = r.json()
128128
rates = data.get('rates') or {}
@@ -135,7 +135,7 @@ def _fetch_frankfurter(self):
135135
return out
136136

137137
def _fetch_floatrates(self):
138-
r = requests.get(FLOATRATES_URL, timeout=5) # Reduced timeout from 10 to 5 seconds
138+
r = requests.get(FLOATRATES_URL, timeout=3) # Further reduced timeout from 5 to 3 seconds
139139
r.raise_for_status()
140140
data = r.json() # keys are lowercase currency codes
141141
out = {'EUR': Decimal('1')}
@@ -151,7 +151,7 @@ def _fetch_floatrates(self):
151151
return out
152152

153153
def _fetch_erapi_open(self):
154-
r = requests.get(ERAPI_URL, timeout=5) # Reduced timeout from 10 to 5 seconds
154+
r = requests.get(ERAPI_URL, timeout=3) # Further reduced timeout from 5 to 3 seconds
155155
r.raise_for_status()
156156
data = r.json()
157157
if data.get('result') != 'success':

app/email.py

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from app import db
77
from apscheduler.schedulers.background import BackgroundScheduler
88
import atexit
9+
import threading
10+
import time
911

1012
def get_currency_symbol(currency_code):
1113
"""Get currency symbol for display"""
@@ -188,15 +190,17 @@ def send_expiry_notification(app, user, subscriptions):
188190
# Log connection attempt
189191
print(f"🔌 Connecting to {app.config['MAIL_SERVER']}:{app.config['MAIL_PORT']}")
190192

191-
# Use SSL for port 465, TLS for other ports
193+
# Use SSL for port 465, TLS for other ports with timeout
194+
smtp_timeout = 10 # 10 second timeout for SMTP operations
195+
192196
if app.config['MAIL_PORT'] == 465:
193197
# Port 465 uses implicit SSL
194198
print("🔒 Using SSL connection (port 465)")
195-
server = smtplib.SMTP_SSL(app.config['MAIL_SERVER'], app.config['MAIL_PORT'])
199+
server = smtplib.SMTP_SSL(app.config['MAIL_SERVER'], app.config['MAIL_PORT'], timeout=smtp_timeout)
196200
else:
197201
# Other ports use explicit TLS or plain connection
198202
print(f"🔐 Using {'TLS' if app.config['MAIL_USE_TLS'] else 'plain'} connection")
199-
server = smtplib.SMTP(app.config['MAIL_SERVER'], app.config['MAIL_PORT'])
203+
server = smtplib.SMTP(app.config['MAIL_SERVER'], app.config['MAIL_PORT'], timeout=smtp_timeout)
200204
if app.config['MAIL_USE_TLS']:
201205
server.starttls()
202206

@@ -217,13 +221,45 @@ def send_expiry_notification(app, user, subscriptions):
217221
print(f"❌ SMTP Connection failed for {user.username}: {e}")
218222
print("🔍 Check MAIL_SERVER and MAIL_PORT")
219223
return False
224+
except smtplib.SMTPServerDisconnected as e:
225+
print(f"❌ SMTP Server disconnected for {user.username}: {e}")
226+
print("🔍 Mail server may be overloaded or timing out")
227+
return False
220228
except smtplib.SMTPException as e:
221229
print(f"❌ SMTP error for {user.username}: {e}")
222230
return False
231+
except ConnectionError as e:
232+
print(f"❌ Network connection error for {user.username}: {e}")
233+
print("🔍 Check network connectivity to mail server")
234+
return False
235+
except TimeoutError as e:
236+
print(f"❌ Timeout error for {user.username}: {e}")
237+
print("🔍 Mail server is taking too long to respond")
238+
return False
223239
except Exception as e:
224240
print(f"❌ Failed to send email to {user.username}: {e}")
225241
return False
226242

243+
def check_expiring_subscriptions_with_timeout(app):
244+
"""Wrapper function to check expiring subscriptions with timeout protection"""
245+
def run_check():
246+
try:
247+
check_expiring_subscriptions(app)
248+
except Exception as e:
249+
print(f"❌ Error in notification check: {e}")
250+
251+
# Run the check in a separate thread with timeout
252+
thread = threading.Thread(target=run_check)
253+
thread.daemon = True
254+
thread.start()
255+
256+
# Wait for completion with timeout
257+
thread.join(timeout=60) # 60 second timeout for entire email check process
258+
259+
if thread.is_alive():
260+
print("⚠️ Email notification check timed out after 60 seconds")
261+
# Thread will continue in background but won't block the scheduler
262+
227263
def check_expiring_subscriptions(app):
228264
"""Check for expiring subscriptions and send notifications"""
229265
with app.app_context():
@@ -310,11 +346,12 @@ def start_scheduler(app):
310346

311347
# Check every hour to respect user-specific notification times
312348
scheduler.add_job(
313-
func=lambda: check_expiring_subscriptions(app),
349+
func=lambda: check_expiring_subscriptions_with_timeout(app),
314350
trigger="interval",
315351
hours=1,
316352
id='check_subscriptions',
317-
replace_existing=True
353+
replace_existing=True,
354+
max_instances=1 # Prevent overlapping runs
318355
)
319356

320357
scheduler.start()
@@ -422,15 +459,17 @@ def send_test_email(app, user):
422459
# Log connection attempt
423460
print(f"🔌 Connecting to {app.config['MAIL_SERVER']}:{app.config['MAIL_PORT']}")
424461

425-
# Use SSL for port 465, TLS for other ports
462+
# Use SSL for port 465, TLS for other ports with timeout
463+
smtp_timeout = 10 # 10 second timeout for SMTP operations
464+
426465
if app.config['MAIL_PORT'] == 465:
427466
# Port 465 uses implicit SSL
428467
print("🔒 Using SSL connection (port 465)")
429-
server = smtplib.SMTP_SSL(app.config['MAIL_SERVER'], app.config['MAIL_PORT'])
468+
server = smtplib.SMTP_SSL(app.config['MAIL_SERVER'], app.config['MAIL_PORT'], timeout=smtp_timeout)
430469
else:
431470
# Other ports use explicit TLS or plain connection
432471
print(f"🔐 Using {'TLS' if app.config['MAIL_USE_TLS'] else 'plain'} connection")
433-
server = smtplib.SMTP(app.config['MAIL_SERVER'], app.config['MAIL_PORT'])
472+
server = smtplib.SMTP(app.config['MAIL_SERVER'], app.config['MAIL_PORT'], timeout=smtp_timeout)
434473
if app.config['MAIL_USE_TLS']:
435474
server.starttls()
436475

@@ -460,13 +499,34 @@ def send_test_email(app, user):
460499
'success': False,
461500
'message': error_msg
462501
}
502+
except smtplib.SMTPServerDisconnected as e:
503+
error_msg = f"Mail server disconnected: {e}. Server may be overloaded."
504+
print(f"❌ {error_msg}")
505+
return {
506+
'success': False,
507+
'message': error_msg
508+
}
463509
except smtplib.SMTPRecipientsRefused as e:
464510
error_msg = f"Recipient refused: {e}. Check email address."
465511
print(f"❌ {error_msg}")
466512
return {
467513
'success': False,
468514
'message': error_msg
469515
}
516+
except ConnectionError as e:
517+
error_msg = f"Network connection error: {e}. Check network connectivity."
518+
print(f"❌ {error_msg}")
519+
return {
520+
'success': False,
521+
'message': error_msg
522+
}
523+
except TimeoutError as e:
524+
error_msg = f"Connection timeout: {e}. Mail server is taking too long to respond."
525+
print(f"❌ {error_msg}")
526+
return {
527+
'success': False,
528+
'message': error_msg
529+
}
470530
except Exception as e:
471531
error_msg = f"Unexpected error: {e}"
472532
print(f"❌ {error_msg}")

config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ class Config:
88

99
# Database connection pool settings for SQLite
1010
SQLALCHEMY_ENGINE_OPTIONS = {
11-
'pool_timeout': 20,
11+
'pool_timeout': 10, # Reduced from 20 to 10 seconds
1212
'pool_recycle': 3600,
1313
'pool_pre_ping': True,
1414
'connect_args': {
15-
'timeout': 30,
15+
'timeout': 20, # Reduced from 30 to 20 seconds
1616
'check_same_thread': False
1717
} if 'sqlite' in (os.environ.get('DATABASE_URL') or 'sqlite:///subscriptions.db') else {}
1818
}

docker-compose.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,9 @@ services:
1616
- DAYS_BEFORE_EXPIRY=${DAYS_BEFORE_EXPIRY}
1717
- PUID=${PUID:-1000}
1818
- PGID=${PGID:-1000}
19+
# Timeout optimization settings
20+
- CURRENCY_REFRESH_MINUTES=${CURRENCY_REFRESH_MINUTES:-1440}
21+
- CURRENCY_PROVIDER_PRIORITY=${CURRENCY_PROVIDER_PRIORITY:-frankfurter,floatrates,erapi_open}
22+
- PERFORMANCE_LOGGING=${PERFORMANCE_LOGGING:-true}
1923
volumes:
2024
- ./data:/app/instance

gunicorn.conf.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
backlog = 2048
66

77
# Worker processes
8-
workers = 2
8+
workers = 3 # Increased from 2 to 3 for better load distribution
99
worker_class = "sync"
1010
worker_connections = 1000
11-
timeout = 60 # Increased from default 30s to 60s
11+
timeout = 120 # Increased from 60s to 120s for longer operations
1212
keepalive = 2
1313

1414
# Restart workers after this many requests, to help prevent memory leaks
15-
max_requests = 1000
15+
max_requests = 500 # Reduced from 1000 to prevent memory buildup
1616
max_requests_jitter = 50
1717

1818
# Logging
@@ -36,9 +36,13 @@
3636
certfile = None
3737

3838
# Worker timeout
39-
graceful_timeout = 30
39+
graceful_timeout = 60 # Increased from 30s to 60s
4040
worker_tmp_dir = "/dev/shm"
4141

42+
# Additional timeout settings for better stability
43+
worker_timeout = 120 # Same as timeout
44+
worker_max_requests_jitter = 50
45+
4246
# Environment
4347
raw_env = [
4448
'FLASK_ENV=production',

0 commit comments

Comments
 (0)