This commit is contained in:
dgtlmoon
2026-01-06 17:06:50 +01:00
parent c5eeafe2dd
commit 5fad56b548
4 changed files with 47 additions and 28 deletions
@@ -67,7 +67,7 @@ def construct_blueprint():
message = "✓ Notification sent successfully and removed from queue."
flash(message, 'notice')
else:
message = "Failed to send notification. It remains scheduled for automatic retry."
message = "Failed to send notification. It has been re-queued for automatic retry."
flash(message, 'error')
return redirect(url_for('notification_dashboard.dashboard'))
@@ -108,7 +108,7 @@ def construct_blueprint():
@notification_dashboard.route("/clear-all", methods=['POST'])
@login_optionally_required
def clear_all_notifications():
"""Clear ALL notifications (pending, retrying, and failed)"""
"""Clear ALL notifications (delivered, pending, retrying, and failed)"""
from changedetectionio.notification.task_queue import clear_all_notifications
result = clear_all_notifications()
@@ -116,8 +116,8 @@ def construct_blueprint():
if 'error' in result:
flash(f"Error clearing notifications: {result['error']}", 'error')
else:
total_cleared = result.get('queue', 0) + result.get('schedule', 0) + result.get('results', 0)
flash(f"Cleared {total_cleared} notification(s) from queue.", 'notice')
total_cleared = result.get('queue', 0) + result.get('schedule', 0) + result.get('results', 0) + result.get('delivered', 0)
flash(f"Cleared {total_cleared} notification(s) (delivered, queued, retrying, and failed).", 'notice')
return redirect(url_for('notification_dashboard.dashboard'))
@@ -1067,14 +1067,17 @@ def retry_notification_now(task_id):
return True
except Exception as e:
# Notification failed - but we already revoked it, so it won't retry automatically
# The failure will be logged and visible in the dashboard
# Notification failed - re-queue it so it doesn't disappear and can retry automatically
logger.warning(f"Failed to send notification for task {task_id}: {e}")
logger.info(f"Task {task_id} already removed from queue (user requested immediate execution)")
logger.info(f"Re-queueing notification for automatic retry after manual send failed")
# Clean up metadata and result
_delete_result(task_id)
_delete_task_metadata(task_id)
# Re-queue the notification for automatic retry with exponential backoff
# This ensures it doesn't disappear from the dashboard and will retry later
try:
result = send_notification_task(notification_data)
logger.info(f"Re-queued notification after failed manual send")
except Exception as queue_error:
logger.error(f"Failed to re-queue notification: {queue_error}")
return False
@@ -1429,9 +1432,14 @@ def send_notification_task(n_object: NotificationContextData):
"""
from changedetectionio.notification.handler import process_notification
from changedetectionio.flask_app import datastore, notification_debug_log, app
from changedetectionio.notification_service import NotificationContextData
from datetime import datetime
import json
# Wrap dict in NotificationContextData if needed (for retried tasks from Huey)
if not isinstance(n_object, NotificationContextData):
n_object = NotificationContextData(n_object)
# Load watch
watch = datastore.data['watching'].get(n_object.get('uuid'))
if not watch:
@@ -132,7 +132,8 @@ class FileStorageTaskManager(HueyTaskManager):
'schedule': 0,
'results': 0,
'retry_attempts': 0,
'task_metadata': 0
'task_metadata': 0,
'delivered': 0
}
if not self.storage_path:
@@ -181,6 +182,14 @@ class FileStorageTaskManager(HueyTaskManager):
os.remove(os.path.join(metadata_dir, f))
cleared['task_metadata'] += 1
# Clear delivered (success) notifications
success_dir = os.path.join(self.storage_path, 'success')
if os.path.exists(success_dir):
for f in os.listdir(success_dir):
if f.startswith('success-') and f.endswith('.json'):
os.remove(os.path.join(success_dir, f))
cleared['delivered'] += 1
return cleared
def store_task_metadata(self, task_id, metadata):
@@ -769,37 +769,39 @@ def test_send_now_button(client, live_server, measure_memory_usage, datastore_pa
# Click "Send Now" button (GET request)
res = client.get(url_for("notification_dashboard.send_now", task_id=task_id), follow_redirects=True)
# Should redirect back to notification dashboard with message
# The notification will still fail (bad SMTP server), but should be executed immediately
# and removed from the retry schedule
# Should redirect back to notification dashboard with error message
# The notification will fail (bad SMTP server) and be re-queued for automatic retry
assert b'Failed to send notification' in res.data, "Should show error message"
assert b're-queued for automatic retry' in res.data, "Should indicate notification was re-queued"
# Wait for the task to be removed from schedule (give revoke time to propagate)
# Wait for the notification to be re-queued (old task revoked, new task created)
# The re-queued notification will have a different task_id
max_wait = 5
start_time = time.time()
task_removed = False
notification_requeued = False
while time.time() - start_time < max_wait:
pending_after = get_pending_notifications(limit=100)
all_retrying = [n for n in pending_after if n.get('status') == 'retrying']
retrying_after = [n for n in all_retrying if n.get('task_id') == task_id]
logging.info(f"[{time.time() - start_time:.1f}s] Total retrying: {len(all_retrying)}, Matching task_id {task_id[:8]}: {len(retrying_after)}")
# Check if there's still a retrying notification for this watch
# (it will have a different task_id after re-queueing)
retrying_for_watch = [n for n in all_retrying if n.get('watch_uuid') == uuid]
if len(retrying_after) == 0:
task_removed = True
logging.info(f"✓ Task {task_id} removed from retry schedule after {time.time() - start_time:.1f}s")
logging.info(f"[{time.time() - start_time:.1f}s] Total retrying: {len(all_retrying)}, For this watch: {len(retrying_for_watch)}")
if len(retrying_for_watch) > 0:
notification_requeued = True
new_task_id = retrying_for_watch[0].get('task_id')
logging.info(f"✓ Notification re-queued with new task_id {new_task_id[:8]}... after {time.time() - start_time:.1f}s")
break
# Log details of the found notification
if retrying_after:
logging.info(f" Found retrying notification: {retrying_after[0].get('retry_at')}")
time.sleep(0.5)
# The task should be gone from schedule (either succeeded or moved to dead letter)
assert task_removed, f"Notification should be removed from retry schedule after 'Send Now', but still found after {max_wait}s"
# The notification should be re-queued (not disappeared)
assert notification_requeued, f"Notification should be re-queued after failed 'Send Now', but not found after {max_wait}s"
logging.info("✓ Send Now button successfully executed notification immediately")
logging.info("✓ Send Now button correctly re-queued failed notification")
client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)